Skip to content

Commit

Permalink
feat: add python apis to agreements models (#32967)
Browse files Browse the repository at this point in the history
* feat: added python apis to agreements models

* feat: add python apis to agreements app

* feat: updated pyton apis and rest api

* feat: fixed python apis and added tests

* test: Added error handling and updated test functions

* style: fixed test formatting

* style: fixed test formatting

* style: fixed test formatting

* feat: Fixed error handling for python APIs

* feat: Fixed error handling for python APIs

* fix: edited python api for agreements app

* style: quality revisions for python apis
  • Loading branch information
ericanwoga authored Oct 4, 2023
1 parent d9af2df commit 3d26512
Show file tree
Hide file tree
Showing 3 changed files with 283 additions and 0 deletions.
172 changes: 172 additions & 0 deletions openedx/core/djangoapps/agreements/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
from opaque_keys.edx.keys import CourseKey

from openedx.core.djangoapps.agreements.models import IntegritySignature
from openedx.core.djangoapps.agreements.models import LTIPIITool
from openedx.core.djangoapps.agreements.models import LTIPIISignature

from .data import LTIToolsReceivingPIIData
from .data import LTIPIISignatureData

log = logging.getLogger(__name__)
User = get_user_model()
Expand Down Expand Up @@ -68,3 +73,170 @@ def get_integrity_signatures_for_course(course_id):
"""
course_key = CourseKey.from_string(course_id)
return IntegritySignature.objects.filter(course_key=course_key)


def create_lti_pii_signature(username, course_id, lti_tools):
"""
Creates an lti pii tool signature. If the signature already exist, do not create a new one.
Arguments:
* course_key (str)
* lti_tools (dict)
* lti_tools_hash (int)
Returns:
* An LTIPIISignature, or None if a signature already exists.
"""
course_key = CourseKey.from_string(course_id)
lti_tools_hash = hash(str(lti_tools))

# if user and course exists, update, otherwise create a new signature
try:
user = User.objects.get(username=username)
LTIPIISignature.objects.get(user=user, course_key=course_key)
except User.DoesNotExist:
return None
except LTIPIISignature.DoesNotExist:
signature = LTIPIISignature.objects.create(
user=user,
course_key=course_key,
lti_tools=lti_tools,
lti_tools_hash=lti_tools_hash)
else:
signature = LTIPIISignature.objects.update(
user=user,
course_key=course_key,
lti_tools=lti_tools,
lti_tools_hash=lti_tools_hash)

return signature


def get_lti_pii_signature(username, course_id):
"""
Get the lti pii signature of a user in a course.
Arguments:
* username (str)
* course_id (str)
Returns:
* An LTIPIISignature object, or None if one does not exist for the
user + course combination.
"""
course_key = CourseKey.from_string(course_id)
try:
user = User.objects.get(username=username)
signature = LTIPIISignature.objects.get(user=user, course_key=course_key)
except (User.DoesNotExist, LTIPIISignature.DoesNotExist):
return None
else:
return LTIPIISignatureData(user=signature.user, course_id=str(signature.course_key),
lti_tools=signature.lti_tools, lti_tools_hash=signature.lti_tools_hash)


def get_pii_receiving_lti_tools(course_id):
"""
Get a course's LTI tools that share PII.
Arguments:
* course_id (str)
Returns:
* A List of LTI tools sharing PII.
"""

course_key = CourseKey.from_string(course_id)
try:
course_ltipiitools = LTIPIITool.objects.get(course_key=course_key).lti_tools
except LTIPIITool.DoesNotExist:
return None

return LTIToolsReceivingPIIData(lii_tools_receiving_pii=course_ltipiitools)


def user_lti_pii_signature_needed(username, course_id):
"""
Determines if a user needs to acknowledge the LTI PII Agreement.
Arguments:
* username (str)
Returns:
* True if the user needs to sign a new acknowledgement.
* False if the acknowledgements are up to date.
"""
course_has_lti_pii_tools = _course_has_lti_pii_tools(course_id)
signature_exists = _user_lti_pii_signature_exists(username, course_id)
signature_out_of_date = _user_signature_out_of_date(username, course_id)

return ((course_has_lti_pii_tools and (not signature_exists)) or
(course_has_lti_pii_tools and signature_exists and signature_out_of_date))


def _course_has_lti_pii_tools(course_id):
"""
Determines if a specifc course has lti tools sharing pii.
Arguments:
* course_id (str)
Returns:
* True if the course does have a list.
* False if the course does not.
"""
course_key = CourseKey.from_string(course_id)
try:
course_lti_pii_tools = LTIPIITool.objects.get(course_key=course_key)
except LTIPIITool.DoesNotExist:
# no entry in the database
return False
else:
# returns True if there are entries, and False if the list is empty
return bool(course_lti_pii_tools.lti_tools)


def _user_lti_pii_signature_exists(username, course_id):
"""
Determines if a user's lti pii signature exists for a specfic course
Arguments:
* username (str)
* course_id (str)
Returns:
* True if user has a signature for the given course.
* False if the user does not have a signature for the given course.
"""
course_key = CourseKey.from_string(course_id)

try:
user = User.objects.get(username=username)
LTIPIISignature.objects.get(user=user, course_key=course_key)
except (User.DoesNotExist, LTIPIISignature.DoesNotExist):
return False
else:
return True # signature exist


def _user_signature_out_of_date(username, course_id):
"""
Determines if a user's existing lti pii signature is out-of-date for a given course.
Arguments:
* username (str)
* course_id (str)
Returns:
* True if signature is out-of-date and needs a new signature.
* False if the user has an up-to-date signature.
"""
course_key = CourseKey.from_string(course_id)

try:
user = User.objects.get(username=username)
user_lti_pii_signature_hash = LTIPIISignature.objects.get(course_key=course_key, user=user).lti_tools_hash
course_lti_pii_tools_hash = LTIPIITool.objects.get(course_key=course_key).lti_tools_hash
except (User.DoesNotExist, LTIPIISignature.DoesNotExist, LTIPIITool.DoesNotExist):
return False
else:
return user_lti_pii_signature_hash != course_lti_pii_tools_hash
23 changes: 23 additions & 0 deletions openedx/core/djangoapps/agreements/data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""
Public data structures for this app.
"""
import attr


@attr.s(frozen=True, auto_attribs=True)
class LTIToolsReceivingPIIData:
"""
Class that stores data about the list of LTI tools sharing PII
"""
lii_tools_receiving_pii: {}


@attr.s(frozen=True, auto_attribs=True)
class LTIPIISignatureData:
"""
Class that stores an lti pii signature
"""
user: str
course_id: str
lti_tools: str
lti_tools_hash: str
88 changes: 88 additions & 0 deletions openedx/core/djangoapps/agreements/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,17 @@
create_integrity_signature,
get_integrity_signature,
get_integrity_signatures_for_course,
get_pii_receiving_lti_tools,
create_lti_pii_signature,
get_lti_pii_signature
)
from openedx.core.djangolib.testing.utils import skip_unless_lms
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, pylint: disable=wrong-import-order
from ..models import (
LTIPIITool,
)
from opaque_keys.edx.keys import CourseKey

LOGGER_NAME = "openedx.core.djangoapps.agreements.api"

Expand Down Expand Up @@ -98,3 +105,84 @@ def _assert_integrity_signature(self, signature):
"""
self.assertEqual(signature.user, self.user)
self.assertEqual(signature.course_key, self.course.id)


@skip_unless_lms
class TestLTIPIISignatureApi(SharedModuleStoreTestCase):
"""
Tests for the lti pii signature API
"""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.user = UserFactory()
cls.course = CourseFactory()
cls.course_id = str(cls.course.id)
cls.lti_tools = {"first_lti_tool": "This is the first tool",
"second_lti_tool": "This is the second tool", }
cls.lti_tools_2 = {"first_lti_tool": "This is the first lti tool",
"second_lti_tool": "This is the second tool",
"third_lti_tool": "This is the third tool", }
LTIPIITool.objects.create(
course_key=CourseKey.from_string(cls.course_id),
lti_tools=cls.lti_tools,
lti_tools_hash=11111,
)

def test_create_lti_pii_signature(self):
"""
Test to check if an lti pii signature is created from a course and its lti tools.
"""
signature = create_lti_pii_signature(self.user.username, self.course_id, self.lti_tools)
self._assert_lti_pii_signature(signature)

def test_create_multiple_lti_pii_signature(self):
"""
Test that lti pii signatures are either created or updated
"""

create_lti_pii_signature(self.user.username, self.course_id, self.lti_tools) # first signature
s1 = get_lti_pii_signature(self.user.username, self.course_id) # retrieve the database entry
create_lti_pii_signature(self.user.username, self.course_id, self.lti_tools_2) # signature with updated tools
s2 = get_lti_pii_signature(self.user.username, self.course_id) # retrieve the updated database entry
self.assertNotEqual(s1, s2) # the signatue retrieved from the database should be the updated version

def _assert_lti_pii_signature(self, signature):
"""
Helper function to assert the returned lti pii signature has the correct
user and course key
"""
self.assertEqual(signature.user, self.user)
self.assertEqual(signature.course_key, self.course.id)


@skip_unless_lms
class TestLTIPIIToolsApi(SharedModuleStoreTestCase):
"""
Tests for the lti pii tool sharing API. To make sure the list of LTI tools can be retreived from the Model.
"""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.course = CourseFactory()
cls.course_id = str(cls.course.id)
cls.lti_tools = {"first_lti_tool": "This is the first tool",
"second_lti_tool": "This is the second tool", }
LTIPIITool.objects.create(
course_key=CourseKey.from_string(cls.course_id),
lti_tools=cls.lti_tools,
lti_tools_hash=11111,
)

def test_get_pii_receiving_lti_tools(self):
"""
Test to check if a course's lti pii tools can be retrieved.
"""
data = get_pii_receiving_lti_tools(self.course_id)
self._assert_ltitools(data.lii_tools_receiving_pii)

def _assert_ltitools(self, lti_list):
"""
Helper function to assert the returned list has the correct tools
"""
self.assertEqual(self.lti_tools, lti_list)

0 comments on commit 3d26512

Please sign in to comment.