Skip to content

Commit

Permalink
Merge branch 'master' into generate_certificate_exceptions-to-drf
Browse files Browse the repository at this point in the history
  • Loading branch information
awais786 authored Oct 5, 2024
2 parents a9e0706 + c34ccff commit 5bc87cd
Show file tree
Hide file tree
Showing 37 changed files with 179 additions and 345 deletions.
2 changes: 1 addition & 1 deletion .readthedocs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ version: 2
build:
os: "ubuntu-22.04"
tools:
python: "3.8"
python: "3.12"

sphinx:
configuration: docs/conf.py
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,3 @@ class CourseSettingsSerializer(serializers.Serializer):
show_min_grade_warning = serializers.BooleanField()
sidebar_html_enabled = serializers.BooleanField()
upgrade_deadline = serializers.DateTimeField(allow_null=True)
use_v2_cert_display_settings = serializers.BooleanField()
2 changes: 0 additions & 2 deletions cms/djangoapps/contentstore/rest_api/v1/views/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,6 @@ def get(self, request: Request, course_id: str):
"show_min_grade_warning": false,
"sidebar_html_enabled": true,
"upgrade_deadline": null,
"use_v2_cert_display_settings": false
}
```
"""
Expand All @@ -112,7 +111,6 @@ def get(self, request: Request, course_id: str):
'course_display_name_with_default': course_block.display_name_with_default,
'platform_name': settings.PLATFORM_NAME,
'licensing_enabled': settings.FEATURES.get("LICENSING", False),
'use_v2_cert_display_settings': settings.FEATURES.get("ENABLE_V2_CERT_DISPLAY_SETTINGS", False),
})

serializer = CourseSettingsSerializer(settings_context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ def test_course_settings_response(self):
"show_min_grade_warning": False,
"upgrade_deadline": None,
"licensing_enabled": False,
"use_v2_cert_display_settings": False,
}

self.assertEqual(response.status_code, status.HTTP_200_OK)
Expand Down
11 changes: 0 additions & 11 deletions cms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -468,17 +468,6 @@
# .. toggle_tickets: https://github.com/openedx/edx-platform/pull/26106
'ENABLE_HELP_LINK': True,

# .. toggle_name: FEATURES['ENABLE_V2_CERT_DISPLAY_SETTINGS']
# .. toggle_implementation: DjangoSetting
# .. toggle_default: False
# .. toggle_description: Whether to use the reimagined certificates_display_behavior and certificate_available_date
# .. settings. Will eventually become the default.
# .. toggle_use_cases: temporary
# .. toggle_creation_date: 2021-07-26
# .. toggle_target_removal_date: 2021-10-01
# .. toggle_tickets: 'https://openedx.atlassian.net/browse/MICROBA-1405'
'ENABLE_V2_CERT_DISPLAY_SETTINGS': False,

# .. toggle_name: FEATURES['ENABLE_INTEGRITY_SIGNATURE']
# .. toggle_implementation: DjangoSetting
# .. toggle_default: False
Expand Down
68 changes: 27 additions & 41 deletions cms/templates/settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@
${show_min_grade_warning | n, dump_js_escaped_json},
${can_show_certificate_available_date_field(context_course) | n, dump_js_escaped_json},
"${upgrade_deadline | n, js_escaped_string}",
${settings.FEATURES.get("ENABLE_V2_CERT_DISPLAY_SETTINGS") | n, dump_js_escaped_json}
);
});
</%block>
Expand Down Expand Up @@ -251,58 +250,45 @@ <h2 class="title-2">${_('Course Schedule')}</h2>
</li>
</ol>

<%
use_v2_cert_display_settings = settings.FEATURES.get("ENABLE_V2_CERT_DISPLAY_SETTINGS", False)
%>
% if can_show_certificate_available_date_field(context_course):
<ol class="list-input">
<li class="field-group field-group-certificate-available" id="certificate-available">
<div class="field date" id="field-certificates-display-behavior">
<label for="certificates-display-behavior">${_("Certificates Display Behavior")}</label>
% if use_v2_cert_display_settings:
<select id="certificates-display-behavior">
<option value="early_no_info">${_("Immediately upon passing")}</option>
<option value="end">${_("End date of course")}</option>
<option value="end_with_date">${_("A date after the course end date")}</option>
</select>
% else:
<input id="certificates-display-behavior" type="text">
% endif
<select id="certificates-display-behavior">
<option value="early_no_info">${_("Immediately upon passing")}</option>
<option value="end">${_("End date of course")}</option>
<option value="end_with_date">${_("A date after the course end date")}</option>
</select>
<span class="tip tip-stacked">${_("Certificates are awarded at the end of a course run")}</span>

% if use_v2_cert_display_settings:
<!-- Collapsible -->
<div class="collapsible">
<div id="certificate-display-behavior-collapsible-trigger" class="collapsible-trigger" role="button" tabindex="0" aria-expanded="false">
<span>
<span class="icon icon-inline fa fa-info-circle" aria-hidden="true"></span>
${_("Read more about this setting")}
</span>
<!-- Collapsible -->
<div class="collapsible">
<div id="certificate-display-behavior-collapsible-trigger" class="collapsible-trigger" role="button" tabindex="0" aria-expanded="false">
<span>
<span class="icon icon-inline fa fa-info-circle" aria-hidden="true"></span>
${_("Read more about this setting")}
</span>
</div>
<div id="certificate-display-behavior-collapsible-content" class="collapsible-content collapsed">
<p>${_("In all configurations of this setting, certificates are generated for learners as soon as they achieve the passing threshold in the course (which can occur before a final assignment based on course design)")}</p>
<div>
<div class="collapsible-description-heading">${_("Immediately upon passing")}</div>
<div class="collapsible-description-description">${_("Learners can access their certificate as soon as they achieve a passing grade above the course grade threshold. Note: learners can achieve a passing grade before encountering all assignments in some course configurations.")}</div>
</div>
<div id="certificate-display-behavior-collapsible-content" class="collapsible-content collapsed">
<p>${_("In all configurations of this setting, certificates are generated for learners as soon as they achieve the passing threshold in the course (which can occur before a final assignment based on course design)")}</p>
<div>
<div class="collapsible-description-heading">${_("Immediately upon passing")}</div>
<div class="collapsible-description-description">${_("Learners can access their certificate as soon as they achieve a passing grade above the course grade threshold. Note: learners can achieve a passing grade before encountering all assignments in some course configurations.")}</div>
</div>
<div>
<div class="collapsible-description-heading">${_("On course end date")}</div>
<div class="collapsible-description-description">${_("Learners with passing grades can access their certificate once the end date of the course has elapsed.")}</div>
</div>
<div>
<div class="collapsible-description-heading">${_("A date after the course end date")}</div>
<div class="collapsible-description-description">${_("Learners with passing grades can access their certificate after the date that you set has elapsed.")}</div>
</div>
<div>
<div class="collapsible-description-heading">${_("On course end date")}</div>
<div class="collapsible-description-description">${_("Learners with passing grades can access their certificate once the end date of the course has elapsed.")}</div>
</div>
<div>
<div class="collapsible-description-heading">${_("A date after the course end date")}</div>
<div class="collapsible-description-description">${_("Learners with passing grades can access their certificate after the date that you set has elapsed.")}</div>
</div>
</div>
% endif
</div>
</div>

% if use_v2_cert_display_settings:
<div class="field date hidden" id="field-certificate-available-date" >
% else:
<div class="field date" id="field-certificate-available-date" >
% endif
<div class="field date hidden" id="field-certificate-available-date" >
<label for="certificate-available-date">${_("Certificates Available Date")}</label>
<input type="text" class="certificate-available-date date start datepicker" id="certificate-available-date" placeholder="MM/DD/YYYY" autocomplete="off" />
<span class="icon icon-inline fa fa-calendar-check-o datepicker-icon" aria-hidden="true"></span>
Expand Down
21 changes: 7 additions & 14 deletions common/djangoapps/student/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -646,21 +646,14 @@ def _is_certificate_earned_but_not_available(course_overview, status):
(bool): True if the user earned the certificate but it's hidden due to display behavior, else False
"""
if settings.FEATURES.get("ENABLE_V2_CERT_DISPLAY_SETTINGS"):
return (
not certificates_viewable_for_course(course_overview)
and CertificateStatuses.is_passing_status(status)
and course_overview.certificates_display_behavior in (
CertificatesDisplayBehaviors.END_WITH_DATE,
CertificatesDisplayBehaviors.END
)
)
else:
return (
not certificates_viewable_for_course(course_overview) and
CertificateStatuses.is_passing_status(status) and
course_overview.certificate_available_date
return (
not certificates_viewable_for_course(course_overview)
and CertificateStatuses.is_passing_status(status)
and course_overview.certificates_display_behavior in (
CertificatesDisplayBehaviors.END_WITH_DATE,
CertificatesDisplayBehaviors.END
)
)


def process_survey_link(survey_link, user):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -583,7 +583,7 @@ def test_verification_signal(self):
"""
Verification signal is sent upon approval.
"""
with mock.patch('openedx_events.learning.signals.IDV_ATTEMPT_APPROVED.send_event') as mock_signal:
with mock.patch('openedx.core.djangoapps.signals.signals.LEARNER_SSO_VERIFIED.send_robust') as mock_signal:
# Begin the pipeline.
pipeline.set_id_verification_status(
auth_entry=pipeline.AUTH_ENTRY_LOGIN,
Expand Down
Binary file modified common/static/data/geoip/GeoLite2-Country.mmdb
Binary file not shown.
1 change: 1 addition & 0 deletions lms/djangoapps/bulk_email/message_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ class BulkEmail(BaseMessageType):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.options['from_address'] = kwargs['context']['from_address']
self.options['transactional'] = True
15 changes: 4 additions & 11 deletions lms/djangoapps/certificates/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import logging
from datetime import datetime

from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Q
Expand Down Expand Up @@ -286,12 +285,9 @@ def certificate_downloadable_status(student, course_key):

course_overview = get_course_overview_or_none(course_key)

if settings.FEATURES.get("ENABLE_V2_CERT_DISPLAY_SETTINGS"):
display_behavior_is_valid = (
course_overview.certificates_display_behavior == CertificatesDisplayBehaviors.END_WITH_DATE
)
else:
display_behavior_is_valid = True
display_behavior_is_valid = (
course_overview.certificates_display_behavior == CertificatesDisplayBehaviors.END_WITH_DATE
)

if (
not certificates_viewable_for_course(course_overview)
Expand Down Expand Up @@ -837,10 +833,7 @@ def can_show_certificate_message(course, student, course_grade, certificates_ena

def _course_uses_available_date(course):
"""Returns if the course has an certificate_available_date set and that it should be used"""
if settings.FEATURES.get("ENABLE_V2_CERT_DISPLAY_SETTINGS"):
display_behavior_is_valid = course.certificates_display_behavior == CertificatesDisplayBehaviors.END_WITH_DATE
else:
display_behavior_is_valid = True
display_behavior_is_valid = course.certificates_display_behavior == CertificatesDisplayBehaviors.END_WITH_DATE

return (
can_show_certificate_available_date_field(course)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ workspace {

grades_app -> signal_handlers "Emits COURSE_GRADE_NOW_PASSED signal"
verify_student_app -> signal_handlers "Emits IDV_ATTEMPT_APPROVED signal"
verify_student_app -> signal_handlers "Emits LEARNER_SSO_VERIFIED signal"
verify_student_app -> signal_handlers "Emits PHOTO_VERIFICATION_APPROVED signal"
student_app -> signal_handlers "Emits ENROLLMENT_TRACK_UPDATED signal"
allowlist -> signal_handlers "Emits APPEND_CERTIFICATE_ALLOWLIST signal"
signal_handlers -> generation_handler "Invokes generate_allowlist_certificate()"
Expand Down
29 changes: 23 additions & 6 deletions lms/djangoapps/certificates/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
from openedx.core.djangoapps.signals.signals import (
COURSE_GRADE_NOW_FAILED,
COURSE_GRADE_NOW_PASSED,
LEARNER_SSO_VERIFIED,
PHOTO_VERIFICATION_APPROVED,
)
from openedx_events.learning.signals import EXAM_ATTEMPT_REJECTED, IDV_ATTEMPT_APPROVED

Expand Down Expand Up @@ -117,17 +119,13 @@ def _listen_for_failing_grade(sender, user, course_id, grade, **kwargs): # pyli
log.info(f'Certificate marked not passing for {user.id} : {course_id} via failing grade')


@receiver(IDV_ATTEMPT_APPROVED, dispatch_uid="learner_track_changed")
def _listen_for_id_verification_status_changed(sender, signal, **kwargs): # pylint: disable=unused-argument
def _handle_id_verification_approved(user):
"""
Listen for a signal indicating that the user's id verification status has changed.
Generate a certificate for the user if they are now verified
"""
if not auto_certificate_generation_enabled():
return

event_data = kwargs.get('idv_attempt')
user = User.objects.get(id=event_data.user.id)

user_enrollments = CourseEnrollment.enrollments_for_user(user=user)
expected_verification_status = IDVerificationService.user_status(user)
expected_verification_status = expected_verification_status['status']
Expand All @@ -145,6 +143,25 @@ def _listen_for_id_verification_status_changed(sender, signal, **kwargs): # pyl
)


@receiver(LEARNER_SSO_VERIFIED, dispatch_uid="sso_learner_verified")
@receiver(PHOTO_VERIFICATION_APPROVED, dispatch_uid="photo_verification_approved")
def _listen_for_sso_verification_approved(sender, user, **kwargs): # pylint: disable=unused-argument
"""
Listen for a signal on SSOVerification indicating that the user has been verified.
"""
_handle_id_verification_approved(user)


@receiver(IDV_ATTEMPT_APPROVED, dispatch_uid="openedx_idv_attempt_approved")
def _listen_for_id_verification_approved_event(sender, signal, **kwargs): # pylint: disable=unused-argument
"""
Listen for an openedx event indicating that the user's id verification status has changed.
"""
event_data = kwargs.get('idv_attempt')
user = User.objects.get(id=event_data.user.id)
_handle_id_verification_approved(user)


@receiver(ENROLLMENT_TRACK_UPDATED)
def _listen_for_enrollment_mode_change(sender, user, course_key, mode, **kwargs): # pylint: disable=unused-argument
"""
Expand Down
26 changes: 1 addition & 25 deletions lms/djangoapps/certificates/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,29 +209,6 @@ def test_with_downloadable_web_cert(self):
"uuid": cert_status["uuid"],
}

@ddt.data(
(False, timedelta(days=2), False, True),
(False, -timedelta(days=2), True, None),
(True, timedelta(days=2), True, None),
)
@ddt.unpack
@patch.dict(settings.FEATURES, {"CERTIFICATES_HTML_VIEW": True})
@patch.dict(settings.FEATURES, {"ENABLE_V2_CERT_DISPLAY_SETTINGS": False})
def test_cert_api_return_v1(self, self_paced, cert_avail_delta, cert_downloadable_status, earned_but_not_available):
"""
Test 'downloadable status'
"""
cert_avail_date = datetime.now(pytz.UTC) + cert_avail_delta
self.course.self_paced = self_paced
self.course.certificate_available_date = cert_avail_date
self.course.save()

self._setup_course_certificate()

downloadable_status = certificate_downloadable_status(self.student, self.course.id)
assert downloadable_status["is_downloadable"] == cert_downloadable_status
assert downloadable_status.get("earned_but_not_available") == earned_but_not_available

@ddt.data(
(True, timedelta(days=2), CertificatesDisplayBehaviors.END_WITH_DATE, True, None),
(False, -timedelta(days=2), CertificatesDisplayBehaviors.EARLY_NO_INFO, True, None),
Expand All @@ -243,8 +220,7 @@ def test_cert_api_return_v1(self, self_paced, cert_avail_delta, cert_downloadabl
)
@ddt.unpack
@patch.dict(settings.FEATURES, {"CERTIFICATES_HTML_VIEW": True})
@patch.dict(settings.FEATURES, {"ENABLE_V2_CERT_DISPLAY_SETTINGS": True})
def test_cert_api_return_v2(
def test_cert_api_return(
self,
self_paced,
cert_avail_delta,
Expand Down
6 changes: 3 additions & 3 deletions lms/djangoapps/certificates/tests/test_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from lms.djangoapps.certificates.models import GeneratedCertificate
from lms.djangoapps.certificates.signals import (
_listen_for_enrollment_mode_change,
_listen_for_id_verification_status_changed,
_handle_id_verification_approved,
listen_for_passing_grade
)
from lms.djangoapps.certificates.tests.factories import CertificateAllowlistFactory
Expand Down Expand Up @@ -272,15 +272,15 @@ def test_listen_for_passing_grade(self):
mock.Mock(return_value={"status": "approved"})
)
@mock.patch("lms.djangoapps.certificates.api.auto_certificate_generation_enabled", mock.Mock(return_value=True))
def test_listen_for_id_verification_status_changed(self):
def test_handle_id_verification_approved(self):
"""
Test stop certificate generation process after the verification status changed by raising a filters exception.
Expected result:
- CertificateCreationRequested is triggered and executes TestStopCertificateGenerationStep.
- The certificate is not generated.
"""
_listen_for_id_verification_status_changed(None, self.user)
_handle_id_verification_approved(self.user)

self.assertFalse(
GeneratedCertificate.objects.filter(
Expand Down
Loading

0 comments on commit 5bc87cd

Please sign in to comment.