diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py index 1a0c61984d..46accda240 100644 --- a/lms/djangoapps/courseware/tests/test_views.py +++ b/lms/djangoapps/courseware/tests/test_views.py @@ -8,11 +8,13 @@ import itertools import json import re from datetime import datetime, timedelta -from uuid import uuid4 - from unittest.mock import MagicMock, PropertyMock, call, create_autospec, patch from urllib.parse import quote, urlencode +from uuid import uuid4 + import ddt +from capa.tests.response_xml_factory import \ + MultipleChoiceResponseXMLFactory from completion.test_utils import CompletionWaffleTestMixin from crum import set_current_request from django.conf import settings @@ -24,6 +26,7 @@ from django.test.client import Client from django.test.utils import override_settings from django.urls import reverse, reverse_lazy from edx_toggles.toggles.testutils import override_waffle_flag, override_waffle_switch +from freezegun import freeze_time from markupsafe import escape from milestones.tests.utils import MilestonesTestCaseMixin from opaque_keys.edx.keys import CourseKey, UsageKey @@ -31,17 +34,47 @@ from pytz import UTC, utc from web_fragments.fragment import Fragment from xblock.core import XBlock from xblock.fields import Scope, String +from xmodule.course_module import ( + COURSE_VISIBILITY_PRIVATE, + COURSE_VISIBILITY_PUBLIC, + COURSE_VISIBILITY_PUBLIC_OUTLINE +) +from xmodule.data import CertificatesDisplayBehaviors +from xmodule.graders import ShowCorrectness +from xmodule.modulestore import ModuleStoreEnum +from xmodule.modulestore.django import modulestore +from xmodule.modulestore.tests.django_utils import ( + TEST_DATA_SPLIT_MODULESTORE, + CourseUserType, + ModuleStoreTestCase, + SharedModuleStoreTestCase +) +from xmodule.modulestore.tests.factories import ( + CourseFactory, + ItemFactory, + check_mongo_calls +) import lms.djangoapps.courseware.views.views as views -from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory # lint-amnesty, pylint: disable=wrong-import-order from common.djangoapps.course_modes.models import CourseMode from common.djangoapps.course_modes.tests.factories import CourseModeFactory -from freezegun import freeze_time # lint-amnesty, pylint: disable=wrong-import-order -from common.djangoapps.student.tests.factories import GlobalStaffFactory -from common.djangoapps.student.tests.factories import RequestFactoryNoCsrf +from common.djangoapps.student.models import CourseEnrollment +from common.djangoapps.student.roles import CourseStaffRole +from common.djangoapps.student.tests.factories import ( + TEST_PASSWORD, + AdminFactory, + CourseEnrollmentFactory, + GlobalStaffFactory, + RequestFactoryNoCsrf, + UserFactory +) +from common.djangoapps.util.tests.test_date_utils import fake_pgettext, fake_ugettext +from common.djangoapps.util.url import reload_django_url_config +from common.djangoapps.util.views import ensure_valid_course_key from lms.djangoapps.certificates import api as certs_api from lms.djangoapps.certificates.models import ( CertificateGenerationConfiguration, + CertificateGenerationCourseSetting, CertificateStatuses ) from lms.djangoapps.certificates.tests.factories import ( @@ -63,7 +96,7 @@ from lms.djangoapps.courseware.toggles import ( COURSEWARE_MICROFRONTEND_COURSE_TEAM_PREVIEW, COURSEWARE_OPTIMIZED_RENDER_XBLOCK, COURSEWARE_USE_LEGACY_FRONTEND, - courseware_mfe_is_advertised, + courseware_mfe_is_advertised ) from lms.djangoapps.courseware.user_state_client import DjangoXBlockUserStateClient from lms.djangoapps.grades.config.waffle import ASSUME_ZERO_GRADE_IF_ABSENT @@ -71,6 +104,7 @@ from lms.djangoapps.grades.config.waffle import waffle_switch as grades_waffle_s from lms.djangoapps.instructor.access import allow_access from lms.djangoapps.verify_student.models import VerificationDeadline from lms.djangoapps.verify_student.services import IDVerificationService +from openedx.core.djangoapps.agreements.toggles import ENABLE_INTEGRITY_SIGNATURE from openedx.core.djangoapps.catalog.tests.factories import CourseFactory as CatalogCourseFactory from openedx.core.djangoapps.catalog.tests.factories import CourseRunFactory, ProgramFactory from openedx.core.djangoapps.content.course_overviews.models import CourseOverview @@ -91,30 +125,12 @@ from openedx.features.course_experience import ( ) from openedx.features.course_experience.tests.views.helpers import add_course_mode from openedx.features.course_experience.url_helpers import ( + ExperienceOption, get_courseware_url, get_learning_mfe_home_url, - make_learning_mfe_courseware_url, - ExperienceOption, + make_learning_mfe_courseware_url ) from openedx.features.enterprise_support.tests.mixins.enterprise import EnterpriseTestConsentRequired -from common.djangoapps.student.models import CourseEnrollment -from common.djangoapps.student.roles import CourseStaffRole -from common.djangoapps.student.tests.factories import TEST_PASSWORD, AdminFactory, CourseEnrollmentFactory, UserFactory -from common.djangoapps.util.tests.test_date_utils import fake_pgettext, fake_ugettext -from common.djangoapps.util.url import reload_django_url_config -from common.djangoapps.util.views import ensure_valid_course_key -from xmodule.course_module import COURSE_VISIBILITY_PRIVATE, COURSE_VISIBILITY_PUBLIC, COURSE_VISIBILITY_PUBLIC_OUTLINE # lint-amnesty, pylint: disable=wrong-import-order -from xmodule.data import CertificatesDisplayBehaviors # lint-amnesty, pylint: disable=wrong-import-order -from xmodule.graders import ShowCorrectness # lint-amnesty, pylint: disable=wrong-import-order -from xmodule.modulestore import ModuleStoreEnum # lint-amnesty, pylint: disable=wrong-import-order -from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order -from xmodule.modulestore.tests.django_utils import ( # lint-amnesty, pylint: disable=wrong-import-order - TEST_DATA_SPLIT_MODULESTORE, - CourseUserType, - ModuleStoreTestCase, - SharedModuleStoreTestCase -) -from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, check_mongo_calls # lint-amnesty, pylint: disable=wrong-import-order QUERY_COUNT_TABLE_BLACKLIST = WAFFLE_TABLES @@ -1928,6 +1944,30 @@ class ProgressPageTests(ProgressPageBaseTests): assert response.cert_status == 'earned_but_not_available' assert response.title == 'Your certificate will be available soon!' + @ddt.data(True, False) + def test_no_certs_generated_and_not_verified(self, waffle_override): + """ + Verify if the learner is not ID Verified, and the certs are not yet generated, + but the learner is eligible, the get_cert_data would return cert status Unverified + """ + CertificateGenerationConfiguration(enabled=True).save() + CertificateGenerationCourseSetting( + course_key=self.course.id, self_generation_enabled=True + ).save() + with override_waffle_flag(ENABLE_INTEGRITY_SIGNATURE, active=waffle_override): + with patch( + 'lms.djangoapps.certificates.api.certificate_downloadable_status', + return_value=self.mock_certificate_downloadable_status() + ): + response = views.get_cert_data(self.user, self.course, CourseMode.VERIFIED, MagicMock(passed=True)) + + if waffle_override: + assert response.cert_status == 'requesting' + assert response.title == 'Congratulations, you qualified for a certificate!' + else: + assert response.cert_status == 'unverified' + assert response.title == 'Certificate unavailable' + def assert_invalidate_certificate(self, certificate): """ Dry method to mark certificate as invalid. And assert the response. """ CertificateInvalidationFactory.create( @@ -3477,47 +3517,40 @@ class DatesTabTestCase(ModuleStoreTestCase): vertical = ItemFactory.create(category='vertical', parent_location=subsection.location) ItemFactory.create(category='problem', parent_location=vertical.location, has_score=True) - with patch('lms.djangoapps.courseware.views.views.get_enrollment') as mock_get_enrollment: - mock_get_enrollment.return_value = { - 'mode': enrollment.mode - } - response = self._get_response(self.course) - self.assertContains(response, subsection.display_name) - # Show the Verification Deadline for verified only - self.assertContains(response, 'Verification Deadline') - # Make sure pill exists for today's date - self.assertContains(response, '
') - # Make sure pill exists for next due assignment - self.assertContains(response, '
') - # No pills for verified enrollments - self.assertNotContains(response, '
') - # Make sure the assignment type is rendered - self.assertContains(response, 'Homework:') + response = self._get_response(self.course) + self.assertContains(response, subsection.display_name) + # Show the Verification Deadline for verified only + self.assertContains(response, 'Verification Deadline') + # Make sure pill exists for today's date + self.assertContains(response, '
') + # Make sure pill exists for next due assignment + self.assertContains(response, '
') + # No pills for verified enrollments + self.assertNotContains(response, '
') + # Make sure the assignment type is rendered + self.assertContains(response, 'Homework:') - enrollment.delete() - enrollment = CourseEnrollmentFactory(course_id=self.course.id, user=self.user, mode=CourseMode.AUDIT) - mock_get_enrollment.return_value = { - 'mode': enrollment.mode - } + enrollment.delete() + enrollment = CourseEnrollmentFactory(course_id=self.course.id, user=self.user, mode=CourseMode.AUDIT) - expected_calls = [ - call('course_id', str(self.course.id)), - call('user_id', self.user.id), - call('is_staff', self.user.is_staff), - ] + expected_calls = [ + call('course_id', str(self.course.id)), + call('user_id', self.user.id), + call('is_staff', self.user.is_staff), + ] - response = self._get_response(self.course) + response = self._get_response(self.course) - mock_set_custom_attribute.assert_has_calls(expected_calls, any_order=True) - self.assertContains(response, subsection.display_name) - # Don't show the Verification Deadline for audit - self.assertNotContains(response, 'Verification Deadline') - # Pill doesn't exist for assignment due tomorrow - self.assertNotContains(response, '
') - # Should have verified pills for audit enrollments - self.assertContains(response, '
') - # Make sure the assignment type is rendered - self.assertContains(response, 'Homework:') + mock_set_custom_attribute.assert_has_calls(expected_calls, any_order=True) + self.assertContains(response, subsection.display_name) + # Don't show the Verification Deadline for audit + self.assertNotContains(response, 'Verification Deadline') + # Pill doesn't exist for assignment due tomorrow + self.assertNotContains(response, '
') + # Should have verified pills for audit enrollments + self.assertContains(response, '
') + # Make sure the assignment type is rendered + self.assertContains(response, 'Homework:') @override_waffle_flag(RELATIVE_DATES_FLAG, active=True) def test_reset_deadlines_banner_displays(self): diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py index 52b8760a83..c73f8c7e23 100644 --- a/lms/djangoapps/courseware/views/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -5,10 +5,11 @@ Courseware views functions import json import logging +import urllib from collections import OrderedDict, namedtuple from datetime import datetime +from urllib.parse import quote_plus -import urllib import bleach import requests from django.conf import settings @@ -22,7 +23,6 @@ from django.shortcuts import redirect from django.template.context_processors import csrf from django.urls import reverse from django.utils.decorators import method_decorator -from urllib.parse import quote_plus # lint-amnesty, pylint: disable=wrong-import-order from django.utils.text import slugify from django.utils.translation import gettext from django.utils.translation import gettext_lazy as _ @@ -45,21 +45,32 @@ from rest_framework.decorators import api_view, throttle_classes from rest_framework.response import Response from rest_framework.throttling import UserRateThrottle from web_fragments.fragment import Fragment +from xmodule.course_module import ( + COURSE_VISIBILITY_PUBLIC, + COURSE_VISIBILITY_PUBLIC_OUTLINE +) +from xmodule.modulestore.django import modulestore +from xmodule.modulestore.exceptions import ( + ItemNotFoundError, + NoPathToItem +) +from xmodule.tabs import CourseTabList +from xmodule.x_module import STUDENT_VIEW -from lms.djangoapps.survey import views as survey_views from common.djangoapps.course_modes.models import CourseMode, get_course_prices from common.djangoapps.edxmako.shortcuts import marketing_link, render_to_response, render_to_string -from lms.djangoapps.edxnotes.helpers import is_feature_enabled +from common.djangoapps.student.models import CourseEnrollment, UserTestGroup +from common.djangoapps.util.cache import cache, cache_if_anonymous +from common.djangoapps.util.course import course_location_from_key +from common.djangoapps.util.db import outer_atomic +from common.djangoapps.util.milestones_helpers import get_prerequisite_courses_display +from common.djangoapps.util.views import ensure_valid_course_key, ensure_valid_usage_key from lms.djangoapps.ccx.custom_exception import CCXLocatorValidationException from lms.djangoapps.certificates import api as certs_api from lms.djangoapps.certificates.data import CertificateStatuses from lms.djangoapps.commerce.utils import EcommerceService from lms.djangoapps.course_goals.models import UserActivity -from lms.djangoapps.course_home_api.toggles import ( - course_home_legacy_is_active, - course_home_mfe_progress_tab_is_active -) -from openedx.features.course_experience.url_helpers import get_learning_mfe_home_url, is_request_from_learning_mfe +from lms.djangoapps.course_home_api.toggles import course_home_legacy_is_active, course_home_mfe_progress_tab_is_active from lms.djangoapps.courseware.access import has_access, has_ccx_coach_role from lms.djangoapps.courseware.access_utils import check_course_open_for_learner, check_public_access from lms.djangoapps.courseware.courses import ( @@ -78,23 +89,24 @@ from lms.djangoapps.courseware.courses import ( ) from lms.djangoapps.courseware.date_summary import verified_upgrade_deadline_link from lms.djangoapps.courseware.exceptions import CourseAccessRedirect, Redirect -from lms.djangoapps.courseware.masquerade import setup_masquerade, is_masquerading_as_specific_student +from lms.djangoapps.courseware.masquerade import is_masquerading_as_specific_student, setup_masquerade from lms.djangoapps.courseware.model_data import FieldDataCache from lms.djangoapps.courseware.models import BaseStudentModuleHistory, StudentModule -from lms.djangoapps.courseware.permissions import ( # lint-amnesty, pylint: disable=unused-import +from lms.djangoapps.courseware.permissions import ( MASQUERADE_AS_STUDENT, VIEW_COURSE_HOME, VIEW_COURSEWARE, - VIEW_XQA_INTERFACE ) - from lms.djangoapps.courseware.toggles import is_courses_default_invite_only_enabled from lms.djangoapps.courseware.user_state_client import DjangoXBlockUserStateClient +from lms.djangoapps.edxnotes.helpers import is_feature_enabled from lms.djangoapps.experiments.utils import get_experiment_user_metadata_context from lms.djangoapps.grades.api import CourseGradeFactory from lms.djangoapps.instructor.enrollment import uses_shib from lms.djangoapps.instructor.views.api import require_global_staff +from lms.djangoapps.survey import views as survey_views from lms.djangoapps.verify_student.services import IDVerificationService +from openedx.core.djangoapps.agreements.toggles import is_integrity_signature_enabled from openedx.core.djangoapps.catalog.utils import get_programs, get_programs_with_type from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.credit.api import ( @@ -102,7 +114,7 @@ from openedx.core.djangoapps.credit.api import ( is_credit_course, is_user_eligible_for_credit ) -from openedx.core.djangoapps.enrollments.api import add_enrollment, get_enrollment # lint-amnesty, pylint: disable=unused-import +from openedx.core.djangoapps.enrollments.api import add_enrollment from openedx.core.djangoapps.enrollments.permissions import ENROLL_IN_COURSE from openedx.core.djangoapps.models.course_details import CourseDetails from openedx.core.djangoapps.plugin_api.views import EdxFragmentView @@ -117,23 +129,17 @@ from openedx.features.content_type_gating.models import ContentTypeGatingConfig from openedx.features.course_duration_limits.access import generate_course_expired_fragment from openedx.features.course_experience import DISABLE_UNIFIED_COURSE_TAB_FLAG, course_home_url_name from openedx.features.course_experience.course_tools import CourseToolsPluginManager -from openedx.features.course_experience.url_helpers import get_courseware_url, ExperienceOption +from openedx.features.course_experience.url_helpers import ( + ExperienceOption, + get_courseware_url, + get_learning_mfe_home_url, + is_request_from_learning_mfe +) from openedx.features.course_experience.utils import dates_banner_should_display from openedx.features.course_experience.views.course_dates import CourseDatesFragmentView from openedx.features.course_experience.waffle import ENABLE_COURSE_ABOUT_SIDEBAR_HTML from openedx.features.course_experience.waffle import waffle as course_experience_waffle from openedx.features.enterprise_support.api import data_sharing_consent_required -from common.djangoapps.student.models import CourseEnrollment, UserTestGroup -from common.djangoapps.util.cache import cache, cache_if_anonymous -from common.djangoapps.util.course import course_location_from_key -from common.djangoapps.util.db import outer_atomic -from common.djangoapps.util.milestones_helpers import get_prerequisite_courses_display -from common.djangoapps.util.views import ensure_valid_course_key, ensure_valid_usage_key -from xmodule.course_module import COURSE_VISIBILITY_PUBLIC, COURSE_VISIBILITY_PUBLIC_OUTLINE # lint-amnesty, pylint: disable=wrong-import-order -from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order -from xmodule.modulestore.exceptions import ItemNotFoundError, NoPathToItem # lint-amnesty, pylint: disable=wrong-import-order -from xmodule.tabs import CourseTabList # lint-amnesty, pylint: disable=wrong-import-order -from xmodule.x_module import STUDENT_VIEW # lint-amnesty, pylint: disable=wrong-import-order from ..context_processor import user_timezone_locale_prefs from ..entrance_exams import user_can_skip_entrance_exam @@ -1248,8 +1254,8 @@ def _downloadable_certificate_message(course, cert_downloadable_status): # lint return _downloadable_cert_data(download_url=cert_downloadable_status['download_url']) -def _missing_required_verification(student, enrollment_mode): - return ( +def _missing_required_verification(student, enrollment_mode, course_key): + return not is_integrity_signature_enabled(course_key) and ( enrollment_mode in CourseMode.VERIFIED_MODES and not IDVerificationService.user_is_verified(student) ) @@ -1272,7 +1278,7 @@ def _certificate_message(student, course, enrollment_mode): # lint-amnesty, pyl if cert_downloadable_status['is_downloadable']: return _downloadable_certificate_message(course, cert_downloadable_status) - if _missing_required_verification(student, enrollment_mode): + if _missing_required_verification(student, enrollment_mode, course.id): return UNVERIFIED_CERT_DATA return REQUESTING_CERT_DATA