Skip to content

Commit

Permalink
Merge branch 'master' into hamzawaleed01/upgrade-edx-enterprise-70f6f5a
Browse files Browse the repository at this point in the history
  • Loading branch information
hamzawaleed01 authored Oct 5, 2023
2 parents 4887f7a + c116371 commit 269f703
Show file tree
Hide file tree
Showing 9 changed files with 389 additions and 19 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ build
\#*\#
.env/
openedx/core/djangoapps/django_comment_common/comment_client/python
openedx/core/djangoapps/cache_toolbox/__pycache__
autodeploy.properties
.ws_migrations_complete
dist
Expand Down
44 changes: 43 additions & 1 deletion lms/djangoapps/courseware/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from pytz import UTC
from openedx.core.djangoapps.waffle_utils.models import WaffleFlagCourseOverrideModel
from rest_framework import status
from rest_framework.test import APIClient
from web_fragments.fragment import Fragment
from xblock.core import XBlock
from xblock.fields import Scope, String
Expand Down Expand Up @@ -76,7 +77,10 @@
from lms.djangoapps.courseware.tests.factories import StudentModuleFactory
from lms.djangoapps.courseware.tests.helpers import MasqueradeMixin, get_expiration_banner_text, set_preview_mode
from lms.djangoapps.courseware.testutils import RenderXBlockTestMixin
from lms.djangoapps.courseware.toggles import COURSEWARE_OPTIMIZED_RENDER_XBLOCK
from lms.djangoapps.courseware.toggles import (
COURSEWARE_MICROFRONTEND_SEARCH_ENABLED,
COURSEWARE_OPTIMIZED_RENDER_XBLOCK,
)
from lms.djangoapps.courseware.user_state_client import DjangoXBlockUserStateClient
from lms.djangoapps.courseware.views.views import (
BasePublicVideoXBlockView,
Expand Down Expand Up @@ -3683,3 +3687,41 @@ def test_get_template_and_context(self):
assert template == 'public_video_share_embed.html'
assert context['fragment'] == fragment
assert context['course'] == self.course


class TestCoursewareMFESearchAPI(SharedModuleStoreTestCase):
"""
Tests the endpoint to fetch the Courseware Search waffle flag enabled status.
"""

def setUp(self):
super().setUp()

self.course = CourseFactory.create()

self.client = APIClient()
self.apiUrl = reverse('courseware_search_enabled_view', kwargs={'course_id': str(self.course.id)})

@override_waffle_flag(COURSEWARE_MICROFRONTEND_SEARCH_ENABLED, active=True)
def test_courseware_mfe_search_enabled(self):
"""
Getter to check if user is allowed to use Courseware Search.
"""

response = self.client.get(self.apiUrl, content_type='application/json')
body = json.loads(response.content.decode('utf-8'))

self.assertEqual(response.status_code, 200)
self.assertEqual(body, {'enabled': True})

@override_waffle_flag(COURSEWARE_MICROFRONTEND_SEARCH_ENABLED, active=False)
def test_is_mfe_search_disabled(self):
"""
Getter to check if user is allowed to use Courseware Search.
"""

response = self.client.get(self.apiUrl, content_type='application/json')
body = json.loads(response.content.decode('utf-8'))

self.assertEqual(response.status_code, 200)
self.assertEqual(body, {'enabled': False})
20 changes: 20 additions & 0 deletions lms/djangoapps/courseware/toggles.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,19 @@
f'{WAFFLE_FLAG_NAMESPACE}.mfe_progress_milestones_streak_celebration', __name__
)

# .. toggle_name: courseware.mfe_courseware_search
# .. toggle_implementation: WaffleFlag
# .. toggle_default: False
# .. toggle_description: Enables Courseware Search on Learning MFE
# .. toggle_use_cases: temporary
# .. toggle_creation_date: 2023-09-28
# .. toggle_target_removal_date: None
# .. toggle_tickets: KBK-20
# .. toggle_warning: None.
COURSEWARE_MICROFRONTEND_SEARCH_ENABLED = CourseWaffleFlag(
f'{WAFFLE_FLAG_NAMESPACE}.mfe_courseware_search', __name__
)

# .. toggle_name: courseware.mfe_progress_milestones_streak_discount_enabled
# .. toggle_implementation: CourseWaffleFlag
# .. toggle_default: False
Expand Down Expand Up @@ -153,3 +166,10 @@ def course_is_invitation_only(courselike) -> bool:

def learning_assistant_is_active(course_key):
return COURSEWARE_LEARNING_ASSISTANT.is_enabled(course_key)


def courseware_mfe_search_is_enabled(course_key=None):
"""
Return whether the courseware.mfe_courseware_search flag is on.
"""
return COURSEWARE_MICROFRONTEND_SEARCH_ENABLED.is_enabled(course_key)
18 changes: 15 additions & 3 deletions lms/djangoapps/courseware/views/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.db.models import Q, prefetch_related_objects
from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
from django.shortcuts import redirect
from django.http import JsonResponse, Http404, HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
from django.template.context_processors import csrf
from django.urls import reverse
from django.utils.decorators import method_decorator
Expand All @@ -38,8 +38,8 @@
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey, UsageKey
from openedx_filters.learning.filters import CourseAboutRenderStarted
from pytz import UTC
from requests.exceptions import ConnectionError, Timeout # pylint: disable=redefined-builtin
from pytz import UTC
from rest_framework import status
from rest_framework.decorators import api_view, throttle_classes
from rest_framework.response import Response
Expand Down Expand Up @@ -87,7 +87,7 @@
from lms.djangoapps.courseware.model_data import FieldDataCache
from lms.djangoapps.courseware.models import BaseStudentModuleHistory, StudentModule
from lms.djangoapps.courseware.permissions import MASQUERADE_AS_STUDENT, VIEW_COURSE_HOME, VIEW_COURSEWARE
from lms.djangoapps.courseware.toggles import course_is_invitation_only
from lms.djangoapps.courseware.toggles import course_is_invitation_only, courseware_mfe_search_is_enabled
from lms.djangoapps.courseware.user_state_client import DjangoXBlockUserStateClient
from lms.djangoapps.courseware.utils import (
_use_new_financial_assistance_flow,
Expand Down Expand Up @@ -2275,3 +2275,15 @@ def get_learner_username(learner_identifier):
learner = User.objects.filter(Q(username=learner_identifier) | Q(email=learner_identifier)).first()
if learner:
return learner.username


@api_view(['GET'])
def courseware_mfe_search_enabled(request, course_id=None):
"""
Simple GET endpoint to expose whether the course may use Courseware Search.
"""

course_key = CourseKey.from_string(course_id) if course_id else None

payload = {"enabled": courseware_mfe_search_is_enabled(course_key)}
return JsonResponse(payload)
10 changes: 10 additions & 0 deletions lms/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,16 @@
),
]

urlpatterns += [
re_path(
r'^courses/{}/courseware-search/enabled/$'.format(
settings.COURSE_ID_PATTERN,
),
courseware_views.courseware_mfe_search_enabled,
name='courseware_search_enabled_view',
),
]

urlpatterns += [
re_path(
r'^courses/{}/lti_tab/(?P<provider_uuid>[^/]+)/$'.format(
Expand Down
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
Loading

0 comments on commit 269f703

Please sign in to comment.