Files
edx-platform/lms/djangoapps/certificates/utils.py
michaelroytman bb299c9521 feat: Remove Use of VERIFIED_NAME_FLAG Waffle Flag and is_verified_enabled Utility
The VERIFIED_NAME_FLAG, the VerifiedNameEnabledView, and the verified_name_enabled key removed from responses for both VerifiedNameView view and VerifiedNameHistoryView
were removed as part https://github.com/edx/edx-name-affirmation/pull/12. This was released in version 2.0.0 of the edx-name-affirmation PyPI package. Please see below for additional context for the removal, copied from the name-affirmation commit message.

The VERIFIED_NAME_FLAG was added as part https://github.com/edx/edx-name-affirmation/pull/12, [MST-801](https://openedx.atlassian.net/browse/MST-801) in order to control the release of the Verified Name project. It was used for a phased roll out by percentage of users.

The release reached a percentage of 50% before it was observed that, due to the way percentage roll out works in django-waffle, the code to create or update VerifiedName records was not working properly. The code was written such that any change to a SoftwareSecurePhotoVerification model instance sent a signal, which was received and handled by the Name Affirmation application. If the VERIFIED_NAME_FLAG was on for the requesting user, a Celery task was launched from the Name Affirmation application to perform the creation of or update to the appropriate VerifiedName model instances based on the verify_student application signal. However, we observed that when SoftwareSecurePhotoVerification records were moved into the "created" or "ready" status, a Celery task in Name Affirmation was created, but when SoftwareSecurePhotoVerification records were moved into the "submitted" status, the corresponding Celery task in Name Affirmation was not created. This caused VerifiedName records to stay in the "pending" state.

The django-waffle waffle flag used by the edx-toggle library implements percentage rollout by setting a cookie in a learner's browser session to assign them to the enabled or disabled group.
It turns out that the code that submits a SoftwareSecurePhotoVerification record, which moves it into the "submitted" state, happens as part of a Celery task in the verify_student application in the edx-platform. Therefore, we believe that because there is no request object in a Celery task, the edx-toggle code is defaulting to the case where there is no request object. In this case, the code checks whether the flag is enabled for everyone when determining whether the flag is enabled. Because of the percentage rollout (i.e. waffle flag not enabled for everyone), the Celery task in Name Affirmation is not created. This behavior was confirmed by logging added as part of https://github.com/edx/edx-name-affirmation/pull/62.

We have determined that we do not need the waffle flag, as we are comfortable that enabling the waffle flag for everyone will fix the issue and are comfortable releasing the feature to all users. For this reason, we are removing references to the flag.

[MST-1130](https://openedx.atlassian.net/browse/MST-1130)
2021-11-01 13:33:55 -04:00

253 lines
9.4 KiB
Python

"""
Certificates utilities
"""
from datetime import datetime
import logging
from edx_name_affirmation.api import get_verified_name, should_use_verified_name_for_certs
from django.conf import settings
from django.urls import reverse
from eventtracking import tracker
from opaque_keys.edx.keys import CourseKey
from pytz import utc
from common.djangoapps.student import models_api as student_api
from lms.djangoapps.certificates.data import CertificateStatuses
from lms.djangoapps.certificates.models import GeneratedCertificate
from openedx.core.djangoapps.content.course_overviews.api import get_course_overview_or_none
from xmodule.data import CertificatesDisplayBehaviors
log = logging.getLogger(__name__)
def emit_certificate_event(event_name, user, course_id, course_overview=None, event_data=None):
"""
Utility function responsible for emitting certificate events.
We currently track the following events:
- `edx.certificate.created` - Emit when a course certificate with the `downloadable` status has been awarded to a
learner.
- `edx.certificate.revoked`- Emit when a course certificate with the `downloadable` status has been taken away from
a learner.
- `edx.certificate.shared` - Emit when a learner shares their course certificate to social media (LinkedIn,
Facebook, or Twitter).
- `edx.certificate.evidence_visited` - Emit when a user (other than the learner who owns a certificate) views a
course certificate (e.g., someone views a course certificate shared on a
LinkedIn profile).
Args:
event_name (String) - Text describing the action/event that we are tracking. Examples include `revoked`,
`created`, etc.
user (User) - The User object of the learner associated with this event.
course_id (CourseLocator) - The course-run key associated with this event.
course_overview (CourseOverview) - Optional. The CourseOverview of the course-run associated with this event.
event_data (dictionary) - Optional. Dictionary containing any additional data we want to be associated with an
event.
"""
event_name = '.'.join(['edx', 'certificate', event_name])
if not course_overview:
course_overview = get_course_overview_or_none(course_id)
context = {
'org_id': course_overview.org,
'course_id': str(course_id)
}
data = {
'user_id': user.id,
'course_id': str(course_id),
'certificate_url': get_certificate_url(user.id, course_id, uuid=event_data['certificate_id'])
}
event_data = event_data or {}
event_data.update(data)
with tracker.get_tracker().context(event_name, context):
tracker.emit(event_name, event_data)
def get_certificate_url(user_id=None, course_id=None, uuid=None, user_certificate=None):
"""
Returns the certificate URL
"""
url = ''
course_overview = get_course_overview_or_none(_safe_course_key(course_id))
if not course_overview:
return url
if has_html_certificates_enabled(course_overview):
url = _certificate_html_url(uuid)
else:
url = _certificate_download_url(user_id, course_id, user_certificate=user_certificate)
return url
def has_html_certificates_enabled(course_overview):
"""
Returns True if HTML certificates are enabled in a course run.
"""
if not settings.FEATURES.get('CERTIFICATES_HTML_VIEW', False):
return False
return course_overview.cert_html_view_enabled
def _certificate_html_url(uuid):
"""
Returns uuid based certificate URL.
"""
return reverse(
'certificates:render_cert_by_uuid', kwargs={'certificate_uuid': uuid}
) if uuid else ''
def _certificate_download_url(user_id, course_id, user_certificate=None):
"""
Returns the certificate download URL
"""
if not user_certificate:
try:
user_certificate = GeneratedCertificate.eligible_certificates.get(
user=user_id,
course_id=_safe_course_key(course_id)
)
except GeneratedCertificate.DoesNotExist:
log.critical(
'Unable to lookup certificate\n'
'user id: %s\n'
'course: %s', str(user_id), str(course_id)
)
if user_certificate:
return user_certificate.download_url
return ''
def _safe_course_key(course_key):
"""
Returns the course key
"""
if not isinstance(course_key, CourseKey):
return CourseKey.from_string(course_key)
return course_key
def should_certificate_be_visible(
certificates_display_behavior,
certificates_show_before_end,
has_ended,
certificate_available_date,
self_paced
):
"""
Returns whether it is acceptable to show the student a certificate download
link for a course, based on provided attributes of the course.
Arguments:
certificates_display_behavior (str): string describing the course's
certificate display behavior.
See CourseFields.certificates_display_behavior.help for more detail.
certificates_show_before_end (bool): whether user can download the
course's certificates before the course has ended.
has_ended (bool): Whether the course has ended.
certificate_available_date (datetime): the date the certificate is available on for the course.
self_paced (bool): Whether the course is self-paced.
"""
if settings.FEATURES.get("ENABLE_V2_CERT_DISPLAY_SETTINGS"):
show_early = (
certificates_display_behavior == CertificatesDisplayBehaviors.EARLY_NO_INFO
or certificates_show_before_end
)
past_available_date = (
certificates_display_behavior == CertificatesDisplayBehaviors.END_WITH_DATE
and certificate_available_date
and certificate_available_date < datetime.now(utc)
)
ended_without_available_date = (
certificates_display_behavior == CertificatesDisplayBehaviors.END
and has_ended
)
else:
show_early = (
certificates_display_behavior in ('early_with_info', 'early_no_info')
or certificates_show_before_end
)
past_available_date = (
certificate_available_date
and certificate_available_date < datetime.now(utc)
)
ended_without_available_date = (certificate_available_date is None) and has_ended
return any((self_paced, show_early, past_available_date, ended_without_available_date))
def certificate_status(generated_certificate):
"""
This returns a dictionary with a key for status, and other information.
If the status is "downloadable", the dictionary also contains
"download_url".
If the student has been graded, the dictionary also contains their
grade for the course with the key "grade".
"""
# Import here instead of top of file since this module gets imported before
# the course_modes app is loaded, resulting in a Django deprecation warning.
from common.djangoapps.course_modes.models import CourseMode # pylint: disable=redefined-outer-name, reimported
if generated_certificate:
cert_status = {
'status': generated_certificate.status,
'mode': generated_certificate.mode,
'uuid': generated_certificate.verify_uuid,
}
if generated_certificate.grade:
cert_status['grade'] = generated_certificate.grade
if generated_certificate.mode == 'audit':
course_mode_slugs = [mode.slug for mode in CourseMode.modes_for_course(generated_certificate.course_id)]
# Short term fix to make sure old audit users with certs still see their certs
# only do this if there if no honor mode
if 'honor' not in course_mode_slugs:
cert_status['status'] = CertificateStatuses.auditing
return cert_status
if generated_certificate.status == CertificateStatuses.downloadable:
cert_status['download_url'] = generated_certificate.download_url
return cert_status
else:
return {'status': CertificateStatuses.unavailable, 'mode': GeneratedCertificate.MODES.honor, 'uuid': None}
def certificate_status_for_student(student, course_id):
"""
This returns a dictionary with a key for status, and other information.
See certificate_status for more information.
"""
try:
generated_certificate = GeneratedCertificate.objects.get(user=student, course_id=course_id)
except GeneratedCertificate.DoesNotExist:
generated_certificate = None
return certificate_status(generated_certificate)
def get_preferred_certificate_name(user):
"""
If the verified name feature is enabled and the user has their preference set to use their
verified name for certificates, return their verified name. Else, return the user's profile
name, or an empty string if it doesn't exist.
"""
name_to_use = student_api.get_name(user.id)
if should_use_verified_name_for_certs(user):
verified_name_obj = get_verified_name(user, is_verified=True)
if verified_name_obj:
name_to_use = verified_name_obj.verified_name
if not name_to_use:
name_to_use = ''
return name_to_use