From 19f79a71657cecf3ad948ba5bcd935463b5ff757 Mon Sep 17 00:00:00 2001 From: irfanuddinahmad Date: Mon, 17 May 2021 22:22:12 +0500 Subject: [PATCH] Added util method for course access verification --- .../enterprise_support/tests/test_utils.py | 140 +++++++++++++++++- openedx/features/enterprise_support/utils.py | 46 +++++- 2 files changed, 183 insertions(+), 3 deletions(-) diff --git a/openedx/features/enterprise_support/tests/test_utils.py b/openedx/features/enterprise_support/tests/test_utils.py index 4b3c716b96..895461cf01 100644 --- a/openedx/features/enterprise_support/tests/test_utils.py +++ b/openedx/features/enterprise_support/tests/test_utils.py @@ -7,12 +7,18 @@ import uuid from unittest import mock import ddt +from completion.models import BlockCompletion +from completion.test_utils import CompletionWaffleTestMixin +from completion.waffle import ENABLE_COMPLETION_TRACKING_SWITCH from django.conf import settings +from django.contrib.sites.models import Site from django.test import TestCase from django.test.utils import override_settings -from django.urls import NoReverseMatch -from edx_toggles.toggles.testutils import override_waffle_flag +from django.urls import NoReverseMatch, reverse +from edx_toggles.toggles.testutils import override_waffle_flag, override_waffle_switch +from opaque_keys.edx.keys import CourseKey, UsageKey +from common.djangoapps.student.models import CourseEnrollment from common.djangoapps.student.tests.factories import UserFactory from openedx.core.djangolib.testing.utils import skip_unless_lms from openedx.features.enterprise_support.tests import FEATURES_WITH_ENTERPRISE_ENABLED @@ -34,11 +40,16 @@ from openedx.features.enterprise_support.utils import ( get_enterprise_slug_login_url, get_provider_login_url, handle_enterprise_cookies_for_logistration, + is_course_accessed, is_enterprise_learner, update_account_settings_context_for_enterprise, update_logistration_context_for_enterprise, update_third_party_auth_context_for_enterprise ) +from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase +from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory + +TEST_PASSWORD = 'test' @ddt.ddt @@ -515,3 +526,128 @@ class TestEnterpriseUtils(TestCase): redirect_url=redirect_url, ) assert not mock_next_login_url.called + + +@override_settings(FEATURES=FEATURES_WITH_ENTERPRISE_ENABLED) +@skip_unless_lms +class TestCourseAccessed(SharedModuleStoreTestCase, CompletionWaffleTestMixin): + """ + Test the course accessed functionality. + + """ + @classmethod + def setUpClass(cls): + """ + Creates a test course that can be used for non-destructive tests + """ + # setUpClassAndTestData() already calls setUpClass on SharedModuleStoreTestCase + # pylint: disable=super-method-not-called + with super().setUpClassAndTestData(): + cls.course = cls.create_test_course() + + @classmethod + def setUpTestData(cls): # lint-amnesty, pylint: disable=super-method-not-called + """Set up and enroll our fake user in the course.""" + cls.user = UserFactory(password=TEST_PASSWORD) + CourseEnrollment.enroll(cls.user, cls.course.id) + cls.site = Site.objects.get_current() + + @classmethod + def create_test_course(cls): + """ + Creates a test course. + """ + course = CourseFactory.create() + with cls.store.bulk_operations(course.id): + chapter = ItemFactory.create(category='chapter', parent_location=course.location) + chapter2 = ItemFactory.create(category='chapter', parent_location=course.location) + sequential = ItemFactory.create(category='sequential', parent_location=chapter.location) + sequential2 = ItemFactory.create(category='sequential', parent_location=chapter.location) + sequential3 = ItemFactory.create(category='sequential', parent_location=chapter2.location) + sequential4 = ItemFactory.create(category='sequential', parent_location=chapter2.location) + vertical = ItemFactory.create(category='vertical', parent_location=sequential.location) + vertical2 = ItemFactory.create(category='vertical', parent_location=sequential2.location) + vertical3 = ItemFactory.create(category='vertical', parent_location=sequential3.location) + vertical4 = ItemFactory.create(category='vertical', parent_location=sequential4.location) + course.children = [chapter, chapter2] + chapter.children = [sequential, sequential2] + chapter2.children = [sequential3, sequential4] + sequential.children = [vertical] + sequential2.children = [vertical2] + sequential3.children = [vertical3] + sequential4.children = [vertical4] + if hasattr(cls, 'user'): + CourseEnrollment.enroll(cls.user, course.id) + return course + + def setUp(self): + """ + Set up for the tests. + """ + super().setUp() + self.client.login(username=self.user.username, password=TEST_PASSWORD) + + @override_waffle_switch(ENABLE_COMPLETION_TRACKING_SWITCH, active=True) + def complete_sequential(self, course, sequential): + """ + Completes provided sequential. + """ + course_key = CourseKey.from_string(str(course.id)) + # Fake a visit to sequence2/vertical2 + block_key = UsageKey.from_string(str(sequential.location)) + if block_key.course_key.run is None: + # Old mongo keys must be annotated with course run info before calling submit_completion: + block_key = block_key.replace(course_key=course_key) + completion = 1.0 + BlockCompletion.objects.submit_completion( + user=self.user, + block_key=block_key, + completion=completion + ) + + def course_home_url(self, course): + """ + Returns the URL for the course's home page. + + Arguments: + course (CourseBlock): The course being tested. + """ + return self.course_home_url_from_string(str(course.id)) + + def course_home_url_from_string(self, course_key_string): + """ + Returns the URL for the course's home page. + + Arguments: + course_key_string (String): The course key as string. + """ + return reverse( + 'openedx.course_experience.course_home', + kwargs={ + 'course_id': course_key_string, + } + ) + + def test_course_accessed_for_visit_course_home(self): + """ + Test that a visit to course home does not fall under course access + """ + response = self.client.get(self.course_home_url(self.course)) + assert response.status_code == 200 + course_accessed = is_course_accessed(self.user, str(self.course.id)) + self.assertFalse(course_accessed) + + @override_settings(LMS_BASE='test_url:9999') + def test_course_accessed_with_completion_api(self): + """ + Tests the course accessed function with completion API functionality + """ + self.override_waffle_switch(True) + + # Course tree + course = self.course + vertical1 = course.children[0].children[0].children[0] + + self.complete_sequential(self.course, vertical1) + course_accessed = is_course_accessed(self.user, str(self.course.id)) + self.assertTrue(course_accessed) diff --git a/openedx/features/enterprise_support/utils.py b/openedx/features/enterprise_support/utils.py index 8274a4709e..c550efe8ef 100644 --- a/openedx/features/enterprise_support/utils.py +++ b/openedx/features/enterprise_support/utils.py @@ -7,7 +7,10 @@ import json from crum import get_current_request from django.conf import settings +from django.contrib.auth import get_backends, login +from django.contrib.sessions.middleware import SessionMiddleware from django.core.cache import cache +from django.http import HttpRequest from django.urls import NoReverseMatch, reverse from django.utils.translation import ugettext as _ from edx_django_utils.cache import TieredCache, get_cache_key @@ -17,11 +20,12 @@ from enterprise.models import EnterpriseCustomer, EnterpriseCustomerUser from social_django.models import UserSocialAuth from common.djangoapps import third_party_auth +from common.djangoapps.student.helpers import get_next_url_for_login_page from lms.djangoapps.branding.api import get_privacy_url from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.djangoapps.user_authn.cookies import standard_cookie_settings from openedx.core.djangolib.markup import HTML, Text -from common.djangoapps.student.helpers import get_next_url_for_login_page +from openedx.features.course_experience.utils import get_course_outline_block_tree, get_resume_block ENTERPRISE_HEADER_LINKS = LegacyWaffleFlag('enterprise', 'enterprise_header_links', __name__) # lint-amnesty, pylint: disable=toggle-missing-annotation @@ -454,3 +458,43 @@ def get_provider_login_url(request, provider_id, redirect_url=None): def fetch_enterprise_customer_by_id(enterprise_uuid): return EnterpriseCustomer.objects.get(uuid=enterprise_uuid) + + +def _create_placeholder_request(user): + """ + Helper method to create a placeholder request. + + Arguments: + user (User): Django User object. + + Returns: + request (HttpRequest): A placeholder request object. + """ + request = HttpRequest() + middleware = SessionMiddleware() + middleware.process_request(request) + request.session.save() + backend = get_backends()[0] + user.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__) + login(request, user) + request.user = user + request.META['SERVER_NAME'] = 'edx.org' + request.META['SERVER_PORT'] = '8080' + return request + + +def is_course_accessed(user, course_id): + """ + Check if the learner accessed the course. + + Arguments: + user (User): Django User object. + course_id (String): The course identifier + + Returns: + (bool): True if course has been accessed by the enterprise learner. + """ + request = _create_placeholder_request(user) + course_outline_root_block = get_course_outline_block_tree(request, course_id, user) + resume_block = get_resume_block(course_outline_root_block) if course_outline_root_block else None + return bool(resume_block)