Skip to content

Commit

Permalink
fix: User should not be shown ID Verification prompt on progress page…
Browse files Browse the repository at this point in the history
… if disabled (openedx#29662)

Co-authored-by: Simon Chen <[email protected]>
  • Loading branch information
schenedx and Simon Chen authored Dec 22, 2021
1 parent 96b2162 commit c85be3f
Show file tree
Hide file tree
Showing 2 changed files with 135 additions and 96 deletions.
167 changes: 100 additions & 67 deletions lms/djangoapps/courseware/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@
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
Expand All @@ -24,24 +26,55 @@
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
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 (
Expand All @@ -63,14 +96,15 @@
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
from lms.djangoapps.grades.config.waffle import waffle_switch as grades_waffle_switch
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
Expand All @@ -91,30 +125,12 @@
)
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

Expand Down Expand Up @@ -1928,6 +1944,30 @@ def test_earned_but_not_available_get_cert_data(self):
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(
Expand Down Expand Up @@ -3477,47 +3517,40 @@ def test_defaults(self, mock_set_custom_attribute):
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, '<div class="pill today">')
# Make sure pill exists for next due assignment
self.assertContains(response, '<div class="pill due-next">')
# No pills for verified enrollments
self.assertNotContains(response, '<div class="pill verified">')
# 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
}
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, '<div class="pill today">')
# Make sure pill exists for next due assignment
self.assertContains(response, '<div class="pill due-next">')
# No pills for verified enrollments
self.assertNotContains(response, '<div class="pill verified">')
# 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)

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)

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)

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, '<div class="pill due-next">')
# Should have verified pills for audit enrollments
self.assertContains(response, '<div class="pill verified">')
# 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, '<div class="pill due-next">')
# Should have verified pills for audit enrollments
self.assertContains(response, '<div class="pill verified">')
# 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):
Expand Down
64 changes: 35 additions & 29 deletions lms/djangoapps/courseware/views/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@

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
Expand All @@ -22,7 +23,6 @@
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 _
Expand All @@ -45,21 +45,32 @@
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 (
Expand All @@ -78,31 +89,32 @@
)
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 (
get_credit_requirement_status,
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
Expand All @@ -117,23 +129,17 @@
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
Expand Down Expand Up @@ -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)
)

Expand All @@ -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
Expand Down

0 comments on commit c85be3f

Please sign in to comment.