fix: Stop showing course certificate buttons to beta testers (#28416)
Beta testers can’t earn course certificates, so they should not see a “Request Certificate” button or other info describing how they can earn a cert. MICROBA-992
This commit is contained in:
@@ -44,6 +44,7 @@ from lms.djangoapps.certificates.api import (
|
||||
)
|
||||
from lms.djangoapps.certificates.data import CertificateStatuses
|
||||
from lms.djangoapps.grades.api import CourseGradeFactory
|
||||
from lms.djangoapps.instructor import access
|
||||
from lms.djangoapps.verify_student.models import VerificationDeadline
|
||||
from lms.djangoapps.verify_student.services import IDVerificationService
|
||||
from lms.djangoapps.verify_student.utils import is_verification_expiring_soon, verification_for_datetime
|
||||
@@ -465,6 +466,10 @@ def _cert_info(user, enrollment, cert_status):
|
||||
"""
|
||||
Implements the logic for cert_info -- split out for testing.
|
||||
|
||||
TODO: replace with a method that lives in the certificates app and combines this logic with
|
||||
openedx.core.djangoapps.certificates.api.can_show_certificate_message and
|
||||
lms.djangoapps.courseware.views.get_cert_data
|
||||
|
||||
Arguments:
|
||||
user (User): A user.
|
||||
enrollment (CourseEnrollment): A course enrollment.
|
||||
@@ -526,6 +531,10 @@ def _cert_info(user, enrollment, cert_status):
|
||||
if not CourseMode.is_eligible_for_certificate(enrollment.mode, status=status):
|
||||
return default_info
|
||||
|
||||
if course_overview and access.is_beta_tester(user, course_overview.id):
|
||||
# Beta testers are not eligible for a course certificate
|
||||
return default_info
|
||||
|
||||
status_dict = {
|
||||
'status': status,
|
||||
'mode': cert_status.get('mode', None),
|
||||
|
||||
@@ -54,6 +54,8 @@ from xmodule.data import CertificatesDisplayBehaviors
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
BETA_TESTER_METHOD = 'common.djangoapps.student.helpers.access.is_beta_tester'
|
||||
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
@ddt.ddt
|
||||
@@ -169,6 +171,45 @@ class CourseEndingTest(ModuleStoreTestCase):
|
||||
assert _cert_info(user, enrollment3, cert_status) == {'status': 'processing', 'show_survey_button': False,
|
||||
'can_unenroll': True}
|
||||
|
||||
def test_cert_info_beta_tester(self):
|
||||
user = UserFactory.create()
|
||||
course = CourseOverviewFactory.create()
|
||||
mode = CourseMode.VERIFIED
|
||||
grade = '0.67'
|
||||
status = CertificateStatuses.downloadable
|
||||
cert = GeneratedCertificateFactory.create(
|
||||
user=user,
|
||||
course_id=course.id,
|
||||
status=status,
|
||||
mode=mode
|
||||
)
|
||||
enrollment = CourseEnrollmentFactory(user=user, course_id=course.id, mode=mode)
|
||||
|
||||
cert_status = {
|
||||
'status': status,
|
||||
'grade': grade,
|
||||
'download_url': cert.download_url,
|
||||
'mode': mode,
|
||||
'uuid': 'blah',
|
||||
}
|
||||
with patch(BETA_TESTER_METHOD, return_value=False):
|
||||
assert _cert_info(user, enrollment, cert_status) == {
|
||||
'status': status,
|
||||
'download_url': cert.download_url,
|
||||
'show_survey_button': False,
|
||||
'grade': grade,
|
||||
'mode': mode,
|
||||
'linked_in_url': None,
|
||||
'can_unenroll': False
|
||||
}
|
||||
|
||||
with patch(BETA_TESTER_METHOD, return_value=True):
|
||||
assert _cert_info(user, enrollment, cert_status) == {
|
||||
'status': 'processing',
|
||||
'show_survey_button': False,
|
||||
'can_unenroll': True
|
||||
}
|
||||
|
||||
@ddt.data(
|
||||
(0.70, 0.60),
|
||||
(0.60, 0.70),
|
||||
@@ -823,7 +864,7 @@ class EnrollInCourseTest(EnrollmentEventTestMixin, CacheIsolationTestCase):
|
||||
assert CourseEnrollment.is_enrolled(user, course_id)
|
||||
self.assert_no_events_were_emitted()
|
||||
|
||||
# Now deactive
|
||||
# Now deactivate
|
||||
enrollment.deactivate()
|
||||
assert not CourseEnrollment.is_enrolled(user, course_id)
|
||||
self.assert_unenrollment_event_was_emitted(user, course_id, course, enrollment)
|
||||
|
||||
@@ -19,7 +19,7 @@ from lms.djangoapps.certificates.models import (
|
||||
from lms.djangoapps.certificates.tasks import CERTIFICATE_DELAY_SECONDS, generate_certificate
|
||||
from lms.djangoapps.certificates.utils import has_html_certificates_enabled
|
||||
from lms.djangoapps.grades.api import CourseGradeFactory
|
||||
from lms.djangoapps.instructor.access import list_with_level
|
||||
from lms.djangoapps.instructor.access import is_beta_tester
|
||||
from lms.djangoapps.verify_student.services import IDVerificationService
|
||||
from openedx.core.djangoapps.content.course_overviews.api import get_course_overview_or_none
|
||||
|
||||
@@ -131,7 +131,7 @@ def _can_generate_regular_certificate(user, course_key, enrollment_mode, course_
|
||||
log.info(f'{course_key} is a CCX course. Certificate cannot be generated for {user.id}.')
|
||||
return False
|
||||
|
||||
if _is_beta_tester(user, course_key):
|
||||
if is_beta_tester(user, course_key):
|
||||
log.info(f'{user.id} is a beta tester in {course_key}. Certificate cannot be generated.')
|
||||
return False
|
||||
|
||||
@@ -268,7 +268,7 @@ def _can_set_regular_cert_status(user, course_key, enrollment_mode):
|
||||
if _is_ccx_course(course_key):
|
||||
return False
|
||||
|
||||
if _is_beta_tester(user, course_key):
|
||||
if is_beta_tester(user, course_key):
|
||||
return False
|
||||
|
||||
return _can_set_cert_status_common(user, course_key, enrollment_mode)
|
||||
@@ -324,14 +324,6 @@ def _can_generate_certificate_for_status(user, course_key, enrollment_mode):
|
||||
return True
|
||||
|
||||
|
||||
def _is_beta_tester(user, course_key):
|
||||
"""
|
||||
Check if the user is a beta tester in this course run
|
||||
"""
|
||||
beta_testers_queryset = list_with_level(course_key, 'beta')
|
||||
return beta_testers_queryset.filter(username=user.username).exists()
|
||||
|
||||
|
||||
def _is_ccx_course(course_key):
|
||||
"""
|
||||
Check if the course is a CCX (custom edX course)
|
||||
|
||||
@@ -32,7 +32,7 @@ from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
BETA_TESTER_METHOD = 'lms.djangoapps.certificates.generation_handler._is_beta_tester'
|
||||
BETA_TESTER_METHOD = 'lms.djangoapps.certificates.generation_handler.is_beta_tester'
|
||||
COURSE_OVERVIEW_METHOD = 'lms.djangoapps.certificates.generation_handler.get_course_overview_or_none'
|
||||
CCX_COURSE_METHOD = 'lms.djangoapps.certificates.generation_handler._is_ccx_course'
|
||||
GET_GRADE_METHOD = 'lms.djangoapps.certificates.generation_handler._get_course_grade'
|
||||
|
||||
@@ -1545,8 +1545,8 @@ class ProgressPageTests(ProgressPageBaseTests):
|
||||
self.assertContains(resp, "Download Your Certificate")
|
||||
|
||||
@ddt.data(
|
||||
(True, 54),
|
||||
(False, 54),
|
||||
(True, 55),
|
||||
(False, 55),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_progress_queries_paced_courses(self, self_paced, query_count):
|
||||
@@ -1559,8 +1559,8 @@ class ProgressPageTests(ProgressPageBaseTests):
|
||||
|
||||
@patch.dict(settings.FEATURES, {'ASSUME_ZERO_GRADE_IF_ABSENT_FOR_ALL_TESTS': False})
|
||||
@ddt.data(
|
||||
(False, 62, 45),
|
||||
(True, 54, 39)
|
||||
(False, 63, 46),
|
||||
(True, 55, 40)
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_progress_queries(self, enable_waffle, initial, subsequent):
|
||||
|
||||
@@ -112,3 +112,11 @@ def update_forum_role(course_id, user, rolename, action):
|
||||
role.users.remove(user)
|
||||
else:
|
||||
raise ValueError(f"unrecognized action '{action}'")
|
||||
|
||||
|
||||
def is_beta_tester(user, course_id):
|
||||
"""
|
||||
Returns True if the user is a beta tester in this course, and False if not
|
||||
"""
|
||||
beta_testers_queryset = list_with_level(course_id, 'beta')
|
||||
return beta_testers_queryset.filter(username=user.username).exists()
|
||||
|
||||
@@ -10,6 +10,7 @@ from common.djangoapps.student.tests.factories import UserFactory
|
||||
from lms.djangoapps.instructor.access import (
|
||||
allow_access,
|
||||
list_with_level,
|
||||
is_beta_tester,
|
||||
revoke_access,
|
||||
update_forum_role
|
||||
)
|
||||
@@ -47,6 +48,12 @@ class TestInstructorAccessList(SharedModuleStoreTestCase):
|
||||
assert set(beta_testers) == set(self.beta_testers)
|
||||
assert set(beta_testers_alternative) == set(self.beta_testers)
|
||||
|
||||
def test_is_beta(self):
|
||||
beta_tester = self.beta_testers[0]
|
||||
user = UserFactory.create()
|
||||
assert is_beta_tester(beta_tester, self.course.id)
|
||||
assert not is_beta_tester(user, self.course.id)
|
||||
|
||||
|
||||
class TestInstructorAccessAllow(EmailTemplateTagMixin, SharedModuleStoreTestCase):
|
||||
""" Test access allow. """
|
||||
|
||||
@@ -11,6 +11,7 @@ from django.conf import settings
|
||||
|
||||
from lms.djangoapps.certificates import api as certs_api
|
||||
from lms.djangoapps.certificates.data import CertificateStatuses
|
||||
from lms.djangoapps.instructor import access
|
||||
from openedx.core.djangoapps.certificates.config import waffle
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
from xmodule.data import CertificatesDisplayBehaviors
|
||||
@@ -34,16 +35,18 @@ def can_show_certificate_message(course, student, course_grade, certificates_ena
|
||||
"""
|
||||
Returns True if a course certificate message can be shown
|
||||
"""
|
||||
is_allowlisted = certs_api.is_on_allowlist(student, course.id)
|
||||
auto_cert_gen_enabled = auto_certificate_generation_enabled()
|
||||
has_active_enrollment = CourseEnrollment.is_enrolled(student, course.id)
|
||||
certificates_are_viewable = certs_api.certificates_viewable_for_course(course)
|
||||
is_beta_tester = access.is_beta_tester(student, course.id)
|
||||
has_passed_or_is_allowlisted = _has_passed_or_is_allowlisted(course, student, course_grade)
|
||||
|
||||
return (
|
||||
(auto_cert_gen_enabled or certificates_enabled_for_course) and
|
||||
has_active_enrollment and
|
||||
certificates_are_viewable and
|
||||
(course_grade.passed or is_allowlisted)
|
||||
has_passed_or_is_allowlisted and
|
||||
(not is_beta_tester)
|
||||
)
|
||||
|
||||
|
||||
@@ -95,3 +98,13 @@ def display_date_for_certificate(course, certificate):
|
||||
|
||||
def is_valid_pdf_certificate(cert_data):
|
||||
return cert_data.cert_status == CertificateStatuses.downloadable and cert_data.download_url
|
||||
|
||||
|
||||
def _has_passed_or_is_allowlisted(course, student, course_grade):
|
||||
"""
|
||||
Returns True if the student has passed this course run, or is on the allowlist for this course run
|
||||
"""
|
||||
is_allowlisted = certs_api.is_on_allowlist(student, course.id)
|
||||
has_passed = course_grade and course_grade.passed
|
||||
|
||||
return has_passed or is_allowlisted
|
||||
|
||||
@@ -7,14 +7,22 @@ from datetime import datetime
|
||||
import ddt
|
||||
import pytz
|
||||
from django.test import TestCase
|
||||
from unittest.mock import patch
|
||||
from edx_toggles.toggles import LegacyWaffleSwitch
|
||||
from edx_toggles.toggles.testutils import override_waffle_switch
|
||||
|
||||
from common.djangoapps.course_modes.models import CourseMode
|
||||
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory
|
||||
from openedx.core.djangoapps.certificates import api
|
||||
from openedx.core.djangoapps.certificates.config import waffle as certs_waffle
|
||||
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
|
||||
from xmodule.data import CertificatesDisplayBehaviors
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
|
||||
|
||||
BETA_TESTER_METHOD = 'openedx.core.djangoapps.certificates.api.access.is_beta_tester'
|
||||
CERTS_VIEWABLE_METHOD = 'openedx.core.djangoapps.certificates.api.certs_api.certificates_viewable_for_course'
|
||||
PASSED_OR_ALLOWLISTED_METHOD = 'openedx.core.djangoapps.certificates.api._has_passed_or_is_allowlisted'
|
||||
|
||||
|
||||
# TODO: Copied from lms.djangoapps.certificates.models,
|
||||
@@ -147,3 +155,33 @@ class CertificatesApiTestCase(TestCase):
|
||||
maybe_avail = self.course.certificate_available_date if uses_avail_date else self.certificate.modified_date
|
||||
assert maybe_avail == api.available_date_for_certificate(self.course, self.certificate)
|
||||
assert self.certificate.modified_date == api.display_date_for_certificate(self.course, self.certificate)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class CertificatesMessagingTestCase(ModuleStoreTestCase):
|
||||
"""
|
||||
API tests for certificate messaging
|
||||
"""
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.course = CourseOverviewFactory.create()
|
||||
self.course_run_key = self.course.id
|
||||
self.user = UserFactory.create()
|
||||
self.enrollment = CourseEnrollmentFactory(
|
||||
user=self.user,
|
||||
course_id=self.course_run_key,
|
||||
is_active=True,
|
||||
mode=CourseMode.VERIFIED,
|
||||
)
|
||||
|
||||
def test_beta_tester(self):
|
||||
grade = None
|
||||
certs_enabled = True
|
||||
|
||||
with patch(PASSED_OR_ALLOWLISTED_METHOD, return_value=True):
|
||||
with patch(CERTS_VIEWABLE_METHOD, return_value=True):
|
||||
with patch(BETA_TESTER_METHOD, return_value=False):
|
||||
assert api.can_show_certificate_message(self.course, self.user, grade, certs_enabled)
|
||||
|
||||
with patch(BETA_TESTER_METHOD, return_value=True):
|
||||
assert not api.can_show_certificate_message(self.course, self.user, grade, certs_enabled)
|
||||
|
||||
Reference in New Issue
Block a user