Follow-up to PR 36789 (#37751)

* refactor(certificates): replace direct model imports with data classes and APIs

* fix: use Certificates API to create certificates

* docs: update docstring for get_certificate_for_user

* fix: remove trailing whitespace

---------

Co-authored-by: coder1918 <ram.chandra@wgu.edu>
Co-authored-by: Deborah Kaplan <deborahgu@users.noreply.github.com>
This commit is contained in:
Daniel Wong
2026-01-08 12:03:46 -06:00
committed by GitHub
parent 7e72ec2778
commit cd6faeb966
20 changed files with 621 additions and 168 deletions

View File

@@ -179,13 +179,10 @@ class CourseEnrollmentQuerySet(models.QuerySet):
def get_user_course_ids_with_certificates(self, username):
"""
Gets user's course ids with certificates.
Retrieve the list of course IDs for which the given user has earned certificates.
"""
from lms.djangoapps.certificates.models import GeneratedCertificate # pylint: disable=import-outside-toplevel
course_ids_with_certificates = GeneratedCertificate.objects.filter(
user__username=username
).values_list('course_id', flat=True)
return course_ids_with_certificates
from lms.djangoapps.certificates.api import get_course_ids_from_certs_for_user
return get_course_ids_from_certs_for_user(username)
class CourseEnrollmentManager(models.Manager):

View File

@@ -24,7 +24,7 @@ from common.djangoapps.student.api import is_user_enrolled_in_course
from common.djangoapps.student.models import CourseEnrollment
from lms.djangoapps.branding import api as branding_api
from lms.djangoapps.certificates.config import AUTO_CERTIFICATE_GENERATION as _AUTO_CERTIFICATE_GENERATION
from lms.djangoapps.certificates.data import CertificateStatuses
from lms.djangoapps.certificates.data import CertificateStatuses, GeneratedCertificateData
from lms.djangoapps.certificates.generation_handler import generate_certificate_task as _generate_certificate_task
from lms.djangoapps.certificates.generation_handler import is_on_certificate_allowlist as _is_on_certificate_allowlist
from lms.djangoapps.certificates.models import (
@@ -32,6 +32,7 @@ from lms.djangoapps.certificates.models import (
CertificateDateOverride,
CertificateGenerationConfiguration,
CertificateGenerationCourseSetting,
CertificateGenerationHistory,
CertificateInvalidation,
CertificateTemplate,
CertificateTemplateAsset,
@@ -129,19 +130,16 @@ def get_certificate_for_user(username, course_key, format_results=True):
Arguments:
username (unicode): The identifier of the user.
course_key (CourseKey): A Course Key.
format_results (boolean): Default True. If False, return the GeneratedCertificate object
instead of the serialized dict representation.
Returns:
A dict containing information about the certificate or, optionally,
A dict containing a serialized representation of the certificate or, optionally,
the GeneratedCertificate object itself.
If there is no GeneratedCertificate object for the user and course combination, returns None.
"""
try:
cert = GeneratedCertificate.eligible_certificates.get(user__username=username, course_id=course_key)
except GeneratedCertificate.DoesNotExist:
return None
if format_results:
return _format_certificate_for_user(username, cert)
else:
return cert
cert = GeneratedCertificate.eligible_certificates.filter(user__username=username, course_id=course_key).first()
return _format_certificate_for_user(username, cert) if cert and format_results else cert
def get_certificate_for_user_id(user, course_id):
@@ -728,16 +726,44 @@ def create_certificate_invalidation_entry(certificate, user_requesting_invalidat
return certificate_invalidation
def get_certificate_invalidation_entry(certificate):
def get_certificate_invalidation_entry(generated_certificate, invalidated_by=None, notes=None, active=None):
"""
Retrieves and returns an certificate invalidation entry for a given certificate id.
Retrieve a certificate invalidation entry for a specified certificate.
Optionally filter the invalidation entry by who invalidated it, notes, and whether the invalidation is active.
Args:
generated_certificate (GeneratedCertificate): The certificate to find the invalidation entry for.
invalidated_by (User, optional): User who invalidated the certificate. Defaults to None.
notes (str, optional): Notes associated with the invalidation. Defaults to None.
active (bool, optional): Whether the invalidation entry is currently active. Defaults to None.
Returns:
CertificateInvalidationEntry or None: The matching invalidation entry if found, else None.
"""
log.info(f"Attempting to retrieve certificate invalidation entry for certificate with id {certificate.id}.")
try:
certificate_invalidation_entry = CertificateInvalidation.objects.get(generated_certificate=certificate)
except ObjectDoesNotExist:
log.warning(f"No certificate invalidation found linked to certificate with id {certificate.id}.")
return None
log.info(
f"Attempting to retrieve certificate invalidation entry "
f"for certificate with id {generated_certificate.id}."
)
cert_filter_args = {
"generated_certificate": generated_certificate
}
if invalidated_by is not None:
cert_filter_args["invalidated_by"] = invalidated_by
if notes is not None:
cert_filter_args["notes"] = notes
if active is not None:
cert_filter_args["active"] = active
certificate_invalidation_entry = CertificateInvalidation.objects.filter(**cert_filter_args).first()
# If object does not exist, add a warning to the logs
if certificate_invalidation_entry is None:
log.warning(f"No certificate invalidation found linked to certificate with id {generated_certificate.id}.")
return certificate_invalidation_entry
@@ -958,3 +984,281 @@ def clear_pii_from_certificate_records_for_user(user):
None
"""
GeneratedCertificate.objects.filter(user=user).update(name="")
def get_cert_history_for_course_id(course_id):
"""
Retrieve all certificate generation history records for the specified course.
Args:
course_id (CourseLocator | CourseKey): The unique identifier for the course.
Returns:
QuerySet[CertificateGenerationHistory]: A queryset of all certificate generation history records
associated with the specified course.
"""
return CertificateGenerationHistory.objects.filter(course_id=course_id)
def is_certificate_generation_enabled():
"""
Checks if certificate generation is currently enabled.
This function queries the `CertificateGenerationConfiguration` model to retrieve the
current configuration and returns whether certificate generation is enabled or not.
Returns:
bool: True if certificate generation is enabled, False otherwise.
"""
return CertificateGenerationConfiguration.current().enabled
def set_certificate_generation_config(enabled=True):
"""
Configures the certificate generation settings.
Args:
enabled (bool): If True, enables certificate generation; if False, disables it. Default is True.
Returns:
CertificateGenerationConfiguration: The created or updated certificate generation configuration object.
"""
return CertificateGenerationConfiguration.objects.create(enabled=enabled)
def get_certificate_generation_history(course_id=None, generated_by=None, instructor_task=None, is_regeneration=None):
"""
Retrieves a queryset of `CertificateGenerationHistory` records, filtered by the provided criteria.
Args:
course_id (Optional[CourseKey]): The unique ID of the course (if filtering by course).
generated_by (Optional[User]): The user who generated the certificate (if filtering by user).
instructor_task (Optional[bool]): Whether the certificate generation was triggered by an instructor task.
is_regeneration (Optional[bool]): Whether the certificate was regenerated.
Returns:
QuerySet[CertificateGenerationHistory]: A queryset of filtered `CertificateGenerationHistory` records.
"""
cert_filter_args = {}
# Only add the filter if the corresponding argument is provided
if course_id is not None:
cert_filter_args["course_id"] = course_id
if generated_by is not None:
cert_filter_args["generated_by"] = generated_by
if instructor_task is not None:
cert_filter_args["instructor_task"] = instructor_task
if is_regeneration is not None:
cert_filter_args["is_regeneration"] = is_regeneration
res = CertificateGenerationHistory.objects.filter(**cert_filter_args)
return res
def create_or_update_certificate_generation_history(course_id, generated_by, instructor_task, is_regeneration):
"""
Creates a new certificate generation history record or updates an existing one.
Args:
course_id (CourseKey): The unique identifier for the course run.
generated_by (User): The user (typically an instructor or admin) who initiated the certificate generation.
instructor_task (str): A descriptor or task identifier for the instructor-related task.
is_regeneration (bool): A flag indicating whether the certificate is being
regenerated (True) or newly generated (False).
Returns:
CertificateGenerationHistory: The created or updated CertificateGenerationHistory instance.
"""
cert_history, created = CertificateGenerationHistory.objects.update_or_create(
course_id=course_id,
generated_by=generated_by,
instructor_task=instructor_task,
is_regeneration=is_regeneration
)
return cert_history
def create_or_update_eligible_certificate_for_user(user, course_id, status):
"""
Create or update an eligible GeneratedCertificate for a user in a specific course.
Args:
user (User): The user for whom the certificate is being created or updated.
course_id (CourseKey): The unique identifier for the course the certificate applies to.
status (str): The status of the certificate (e.g., "downloadable", "issued").
Returns:
tuple: A tuple containing:
- `GeneratedCertificate`: The created or updated certificate.
- `bool`: A boolean indicating whether the certificate was created (True) or updated (False).
"""
cert, created = GeneratedCertificate.eligible_certificates.update_or_create(
user=user,
course_id=course_id,
status=status
)
return cert, created
def get_cert_invalidations_for_course(course_key):
"""
Retrieves all certificate invalidations associated with the specified course.
Args:
course_key (CourseKey): The unique identifier for the course run.
Returns:
QuerySet[CertificateInvalidation]: A queryset containing all invalidations for the specified course.
"""
return CertificateInvalidation.get_certificate_invalidations(course_key)
def get_certificates_for_course_and_users(course_id, users):
"""
Retrieves all GeneratedCertificate records for a specific course and a list of users.
Args:
course_id (CourseKey): The unique identifier for the course run.
users (Iterable[User]): A list or queryset of User instances to filter certificates by.
Returns:
QuerySet[GeneratedCertificate]: A queryset containing the matching certificate records.
"""
return GeneratedCertificate.objects.filter(course_id=course_id, user__in=users)
def get_course_ids_from_certs_for_user(username):
"""
Retrieves a list of course IDs for which the given user has generated certificates.
Args:
username (str): The username of the user whose course IDs are being retrieved.
Returns:
list: A list of course IDs for which the user has generated certificates.
If no certificates are found, an empty list is returned.
"""
course_ids_with_certificates = GeneratedCertificate.objects.filter(
user__username=username
).values_list('course_id', flat=True)
return list(course_ids_with_certificates)
def get_generated_certificate(user, course_id):
"""
Retrieves the GeneratedCertificateData for the given user and course.
Args:
user (User): The user for whom the certificate is being fetched.
course_id (CourseKey): The unique identifier for the course.
Returns:
Optional[GeneratedCertificateData]: A `GeneratedCertificateData` object if found, otherwise None.
"""
try:
cert = GeneratedCertificate.objects.get(user=user, course_id=course_id)
return GeneratedCertificateData(
user=cert.user,
course_id=cert.course_id
)
except GeneratedCertificate.DoesNotExist:
return None
def create_generated_certificate(cert_args):
"""
Creates a new `GeneratedCertificate` object using the provided arguments.
Args:
cert_args (dict): A dictionary containing the certificate's attributes, such as
"user", "course_id", "status", etc.
Returns:
GeneratedCertificate: The newly created `GeneratedCertificate` object.
Raises:
ValueError: If any required fields ("user", "course_id") are missing from `cert_args`.
"""
required_fields = {"user", "course_id"}
# Check if all required fields are present
if not all(field in cert_args for field in required_fields):
raise ValueError(f"Missing required fields: {required_fields - cert_args.keys()}")
# Create and return the GeneratedCertificate object
return GeneratedCertificate.objects.create(**cert_args)
def get_eligible_certificate(user, course_id):
"""
Retrieves the eligible GeneratedCertificate for a given user and course.
Args:
user (User): The user object for whom the certificate is being retrieved.
course_id (CourseKey): The unique identifier for the course run.
Returns:
GeneratedCertificate or None: The eligible certificate instance if one exists; otherwise, None.
"""
try:
return GeneratedCertificate.eligible_certificates.get(
user=user.id,
course_id=course_id
)
except GeneratedCertificate.DoesNotExist:
return None
def get_eligible_and_available_certificates(user):
"""
Retrieves all eligible and available certificates for the specified user.
Args:
user (User): The user whose eligible and available certificates are being retrieved.
Returns:
QuerySet[GeneratedCertificate]: A queryset containing all eligible and available certificates
for the specified user.
"""
return GeneratedCertificate.eligible_available_certificates.filter(user=user)
def get_certificates_by_course_and_status(course_id, status):
"""
Retrieves all eligible certificates for a specific course and status.
Args:
course_id (CourseKey): The unique identifier for the course run.
status (str): The status of the certificates to filter by (e.g., "downloadable", "issued").
Returns:
QuerySet[GeneratedCertificate]: A queryset containing the eligible certificates
matching the provided course ID and status.
"""
return GeneratedCertificate.eligible_certificates.filter(
course_id=course_id,
status=status
)
def get_unique_certificate_statuses(course_key):
"""
Retrieves the unique certificate statuses for the specified course.
Args:
course_key (CourseKey): The unique identifier for the course run.
Returns:
QuerySet[str]: A queryset containing the unique certificate statuses for the specified course.
"""
return GeneratedCertificate.get_unique_statuses(course_key=course_key)

View File

@@ -4,6 +4,12 @@ Certificates Data
This provides Data models to represent Certificates data.
"""
from dataclasses import dataclass
from opaque_keys.edx.keys import CourseKey
from django.contrib.auth import get_user_model
User = get_user_model()
class CertificateStatuses:
"""
@@ -81,3 +87,51 @@ class CertificateStatuses:
bool: True if the status is refundable.
"""
return status not in cls.NON_REFUNDABLE_STATUSES
@dataclass
class GeneratedCertificateData:
"""
A data representation of a generated course certificate.
This class encapsulates the essential fields related to a user's generated
certificate, including course information, user identity, certificate status,
grade, and download metadata.
Attributes:
user (User): The user who earned the certificate.
course_id (CourseKey): Identifier for the course associated with the certificate.
verify_uuid (str): UUID used to verify the certificate.
grade (str): The grade achieved in the course.
key (str): Internal key identifier for the certificate.
distinction (bool): Whether the certificate was issued with distinction.
status (str): Current status of the certificate (e.g., 'downloadable', 'unavailable').
mode (str): Enrollment mode at the time of certificate issuance (e.g., 'honor', 'verified').
name (str): Full name as it appears on the certificate.
created_date (str): Timestamp for when the certificate was created.
modified_date (str): Timestamp for when the certificate was last modified.
download_uuid (str): UUID used for generating the download URL.
download_url (str): Direct URL to download the certificate.
error_reason (str): Reason for any certificate generation failure, if applicable.
Methods:
validate_mode(): Validates that the mode is within the supported set of enrollment modes.
"""
user: User
course_id: CourseKey
verify_uuid: str = ""
grade: str = ""
key: str = ""
distinction: bool = False
status: str = "unavailable"
mode: str = "honor"
name: str = ""
created_date: str = None
modified_date: str = None
download_uuid: str = ""
download_url: str = ""
error_reason: str = ""
def validate_mode(self):
if self.mode not in self.MODES:
raise ValueError(f"Invalid mode: {self.mode}")

View File

@@ -46,6 +46,7 @@ from lms.djangoapps.certificates.api import (
get_certificate_invalidation_entry,
get_certificate_url,
get_certificates_for_user,
get_course_ids_from_certs_for_user,
get_certificates_for_user_by_course_keys,
has_self_generated_certificates_enabled,
is_certificate_invalidated,
@@ -1275,3 +1276,46 @@ class CertificatesLearnerRetirementFunctionality(ModuleStoreTestCase):
cert_course2 = GeneratedCertificate.objects.get(user=self.user, course_id=self.course2.id)
assert cert_course1.name == ""
assert cert_course2.name == ""
class GetCourseIdsForUsernameTests(TestCase):
"""
Test suite for the `get_course_ids_from_certs_for_user` function.
"""
def setUp(self):
"""
Set up a user and two course certificates for testing.
Creates a test user using a factory and generates two course certificates
associated with distinct course keys.
"""
self.user = UserFactory()
self.course_key_1 = CourseKey.from_string("course-v1:some+fake+course1")
self.course_key_2 = CourseKey.from_string("course-v1:some+fake+course2")
GeneratedCertificate.objects.create(user=self.user, course_id=self.course_key_1)
GeneratedCertificate.objects.create(user=self.user, course_id=self.course_key_2)
def test_returns_correct_course_ids(self):
"""
Test that the function returns all course IDs for which the user has certificates.
Verifies that both course keys created in setUp are returned when the
user's username is passed to the function.
"""
course_ids = get_course_ids_from_certs_for_user(self.user)
self.assertIn(self.course_key_1, course_ids)
self.assertIn(self.course_key_2, course_ids)
self.assertEqual(len(course_ids), 2)
def test_returns_empty_for_unknown_user(self):
"""
Test that the function returns an empty list if the user has no certificates.
Uses a non-existent username to ensure that the function does not raise
errors and returns an empty list as expected.
"""
course_ids = get_course_ids_from_certs_for_user("nonexistentuser")
self.assertEqual(course_ids, [])

View File

@@ -57,11 +57,7 @@ from common.djangoapps.util.tests.test_date_utils import fake_pgettext, fake_uge
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.data import CertificateStatuses
from lms.djangoapps.certificates.tests.factories import (
CertificateAllowlistFactory,
CertificateInvalidationFactory,
@@ -320,6 +316,7 @@ class CoursewareIndexTestCase(BaseViewsTestCase):
"""
Tests for the courseware index view, used for instructor previews.
"""
def setUp(self):
super().setUp()
self._create_global_staff_user() # this view needs staff permission
@@ -1135,7 +1132,7 @@ class ProgressPageTests(ProgressPageBaseTests):
self.assertNotContains(resp, 'Request Certificate')
# Enable the feature, but do not enable it for this course
CertificateGenerationConfiguration(enabled=True).save()
certs_api.set_certificate_generation_config(enabled=True)
resp = self._get_progress_page()
self.assertNotContains(resp, 'Request Certificate')
@@ -1160,7 +1157,7 @@ class ProgressPageTests(ProgressPageBaseTests):
)
# Enable the feature, but do not enable it for this course
CertificateGenerationConfiguration(enabled=True).save()
certs_api.set_certificate_generation_config(enabled=True)
# Enable certificate generation for this course
certs_api.set_cert_generation_enabled(self.course.id, True)
@@ -1191,7 +1188,7 @@ class ProgressPageTests(ProgressPageBaseTests):
)
# Enable the feature, but do not enable it for this course
CertificateGenerationConfiguration(enabled=True).save()
certs_api.set_certificate_generation_config(enabled=True)
# Enable certificate generation for this course
certs_api.set_cert_generation_enabled(self.course.id, True)
@@ -1281,7 +1278,7 @@ class ProgressPageTests(ProgressPageBaseTests):
@ddt.unpack
def test_show_certificate_request_button(self, course_mode, user_verified):
"""Verify that the Request Certificate is not displayed in audit mode."""
CertificateGenerationConfiguration(enabled=True).save()
certs_api.set_certificate_generation_config(enabled=True)
certs_api.set_cert_generation_enabled(self.course.id, True)
CourseEnrollment.enroll(self.user, self.course.id, mode=course_mode)
with patch(
@@ -1547,10 +1544,8 @@ class ProgressPageTests(ProgressPageBaseTests):
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()
certs_api.set_certificate_generation_config(enabled=True)
certs_api.set_cert_generation_enabled(self.course.id, True)
with patch.dict(settings.FEATURES, ENABLE_CERTIFICATES_IDV_REQUIREMENT=enable_cert_idv_requirement):
with patch(
'lms.djangoapps.certificates.api.certificate_downloadable_status',
@@ -1589,7 +1584,7 @@ class ProgressPageTests(ProgressPageBaseTests):
status=CertificateStatuses.downloadable,
mode=mode
)
CertificateGenerationConfiguration(enabled=True).save()
certs_api.set_certificate_generation_config(enabled=True)
certs_api.set_cert_generation_enabled(self.course.id, True)
return generated_certificate
@@ -2249,6 +2244,7 @@ class TestRenderXBlock(RenderXBlockTestMixin, ModuleStoreTestCase, CompletionWaf
This class overrides the get_response method, which is used by
the tests defined in RenderXBlockTestMixin.
"""
def setUp(self):
reload_django_url_config()
super().setUp()
@@ -2520,6 +2516,7 @@ class TestBasePublicVideoXBlock(ModuleStoreTestCase):
"""
Tests for public video xblock.
"""
def setup_course(self, enable_waffle=True):
"""
Helper method to create the course.
@@ -2563,6 +2560,7 @@ class TestRenderPublicVideoXBlock(TestBasePublicVideoXBlock):
"""
Tests for the courseware.render_public_video_xblock endpoint.
"""
def get_response(self, usage_key, is_embed):
"""
Overridable method to get the response from the endpoint that is being tested.
@@ -2614,6 +2612,7 @@ class TestRenderXBlockSelfPaced(TestRenderXBlock): # lint-amnesty, pylint: disa
Test rendering XBlocks for a self-paced course. Relies on the query
count assertions in the tests defined by RenderXBlockMixin.
"""
def setUp(self): # lint-amnesty, pylint: disable=useless-super-delegation
super().setUp()
@@ -2627,6 +2626,7 @@ class EnterpriseConsentTestCase(EnterpriseTestConsentRequired, ModuleStoreTestCa
"""
Ensure that the Enterprise Data Consent redirects are in place only when consent is required.
"""
def setUp(self):
super().setUp()
self.user = UserFactory.create()
@@ -2727,6 +2727,7 @@ class DatesTabTestCase(TestCase):
"""
Ensure that the legacy dates view redirects appropriately (it no longer exists).
"""
def test_legacy_redirect(self):
"""
Verify that the legacy dates page redirects to the MFE correctly.
@@ -2771,6 +2772,7 @@ class ContentOptimizationTestCase(ModuleStoreTestCase):
"""
Test our ability to make browser optimizations based on XBlock content.
"""
def setUp(self):
super().setUp()
self.math_html_usage_keys = []

View File

@@ -35,7 +35,10 @@ from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, U
from common.djangoapps.student.tests.factories import InstructorFactory
from common.djangoapps.student.tests.factories import StaffFactory
from lms.djangoapps.certificates.data import CertificateStatuses
from lms.djangoapps.certificates.models import GeneratedCertificate
from lms.djangoapps.certificates.api import (
get_certificate_for_user_id,
create_or_update_eligible_certificate_for_user
)
from lms.djangoapps.grades.config.waffle import BULK_MANAGEMENT, WRITABLE_GRADEBOOK
from lms.djangoapps.grades.constants import GradeOverrideFeatureEnum
from lms.djangoapps.grades.course_data import CourseData
@@ -1819,11 +1822,12 @@ class GradebookBulkUpdateViewTest(GradebookViewTestBase):
Test that when we update a user's grade to failing, their certificate is marked notpassing
"""
with override_waffle_flag(self.waffle_flag, active=True):
GeneratedCertificate.eligible_certificates.create(
user=self.student,
course_id=self.course.id,
status=CertificateStatuses.downloadable,
)
cert_args = {
"user": self.student,
"course_id": self.course.id,
"status": CertificateStatuses.downloadable,
}
create_or_update_eligible_certificate_for_user(**cert_args)
self.login_staff()
post_data = [
{
@@ -1853,7 +1857,7 @@ class GradebookBulkUpdateViewTest(GradebookViewTestBase):
content_type='application/json',
)
assert status.HTTP_202_ACCEPTED == resp.status_code
cert = GeneratedCertificate.certificate_for_student(self.student, self.course.id)
cert = get_certificate_for_user_id(self.student, self.course.id)
assert cert.status == CertificateStatuses.notpassing

View File

@@ -21,11 +21,6 @@ from common.djangoapps.student.tests.factories import InstructorFactory
from common.djangoapps.student.tests.factories import UserFactory
from lms.djangoapps.certificates import api as certs_api
from lms.djangoapps.certificates.data import CertificateStatuses
from lms.djangoapps.certificates.models import (
CertificateGenerationConfiguration,
CertificateInvalidation,
GeneratedCertificate
)
from lms.djangoapps.certificates.tests.factories import (
CertificateAllowlistFactory,
CertificateInvalidationFactory,
@@ -64,7 +59,7 @@ class CertificateTaskViewTests(SharedModuleStoreTestCase):
cache.clear()
# Enable the certificate generation feature
CertificateGenerationConfiguration.objects.create(enabled=True)
certs_api.set_certificate_generation_config(enabled=True)
def _login_as(self, role):
"""
@@ -182,7 +177,7 @@ class CertificatesInstructorDashTest(SharedModuleStoreTestCase):
cache.clear()
# Enable the certificate generation feature
CertificateGenerationConfiguration.objects.create(enabled=True)
certs_api.set_certificate_generation_config(enabled=True)
def test_visible_only_to_global_staff(self):
# Instructors don't see the certificates section
@@ -195,7 +190,7 @@ class CertificatesInstructorDashTest(SharedModuleStoreTestCase):
def test_visible_only_when_feature_flag_enabled(self):
# Disable the feature flag
CertificateGenerationConfiguration.objects.create(enabled=False)
certs_api.set_certificate_generation_config(enabled=False)
cache.clear()
# Now even global staff can't see the certificates section
@@ -357,7 +352,7 @@ class CertificatesInstructorApiTest(SharedModuleStoreTestCase):
# Enable certificate generation
cache.clear()
CertificateGenerationConfiguration.objects.create(enabled=True)
certs_api.set_certificate_generation_config(enabled=True)
@ddt.data('enable_certificate_generation')
def test_allow_only_global_staff(self, url_name):
@@ -463,8 +458,8 @@ class CertificatesInstructorApiTest(SharedModuleStoreTestCase):
# Assert success message
assert res_json['message'] ==\
'Certificate regeneration task has been started.' \
' You can view the status of the generation task in the "Pending Tasks" section.'
'Certificate regeneration task has been started.' \
' You can view the status of the generation task in the "Pending Tasks" section.'
def test_certificate_regeneration_error(self):
"""
@@ -493,7 +488,7 @@ class CertificatesInstructorApiTest(SharedModuleStoreTestCase):
# Assert Error Message
assert res_json['message'] ==\
'Please select certificate statuses from the list only.'
'Please select certificate statuses from the list only.'
# Access the url passing 'certificate_statuses' that are not present in db
url = reverse('start_certificate_regeneration', kwargs={'course_id': str(self.course.id)})
@@ -550,7 +545,7 @@ class CertificateExceptionViewInstructorApiTest(SharedModuleStoreTestCase):
# Enable certificate generation
cache.clear()
CertificateGenerationConfiguration.objects.create(enabled=True)
certs_api.set_certificate_generation_config(enabled=True)
self.client.login(username=self.global_staff.username, password=self.TEST_PASSWORD)
def test_certificate_exception_added_successfully(self):
@@ -778,7 +773,7 @@ class CertificateExceptionViewInstructorApiTest(SharedModuleStoreTestCase):
assert not res_json['success']
# Assert Error Message
assert res_json['message'] ==\
'The record is not in the correct format. Please add a valid username or email address.'
'The record is not in the correct format. Please add a valid username or email address.'
def test_remove_certificate_exception_non_existing_error(self):
"""
@@ -869,7 +864,7 @@ class GenerateCertificatesInstructorApiTest(SharedModuleStoreTestCase):
# Enable certificate generation
cache.clear()
CertificateGenerationConfiguration.objects.create(enabled=True)
certs_api.set_certificate_generation_config(enabled=True)
self.client.login(username=self.global_staff.username, password=self.TEST_PASSWORD)
def test_generate_certificate_exceptions_all_students(self):
@@ -1035,7 +1030,7 @@ class TestCertificatesInstructorApiBulkAllowlist(SharedModuleStoreTestCase):
data = json.loads(response.content.decode('utf-8'))
assert len(data['general_errors']) != 0
assert data['general_errors'][0] ==\
'Make sure that the file you upload is in CSV format with no extraneous characters or rows.'
'Make sure that the file you upload is in CSV format with no extraneous characters or rows.'
def test_bad_file_upload_type(self):
"""
@@ -1206,20 +1201,20 @@ class CertificateInvalidationViewTests(SharedModuleStoreTestCase):
# Verify that CertificateInvalidation record has been created in the database i.e. no DoesNotExist error
try:
CertificateInvalidation.objects.get(
generated_certificate=self.generated_certificate,
invalidated_by=self.global_staff,
notes=self.notes,
active=True,
)
cert_filter_args = {
"generated_certificate": self.generated_certificate,
"invalidated_by": self.global_staff,
"notes": self.notes,
"active": True
}
certs_api.get_certificate_invalidation_entry(**cert_filter_args)
except ObjectDoesNotExist:
self.fail("The certificate is not invalidated.")
# Validate generated certificate was invalidated
generated_certificate = GeneratedCertificate.eligible_certificates.get(
user=self.enrolled_user_1,
course_id=self.course.id,
)
# Check if the generated certificate was invalidated
generated_certificate = certs_api.get_certificate_for_user(self.enrolled_user_1, self.course.id, False)
assert not generated_certificate.is_valid()
def test_missing_username_and_email_error(self):
@@ -1345,12 +1340,18 @@ class CertificateInvalidationViewTests(SharedModuleStoreTestCase):
assert response.status_code == 204
# Verify that certificate invalidation successfully removed from database
with pytest.raises(ObjectDoesNotExist):
CertificateInvalidation.objects.get(
generated_certificate=self.generated_certificate,
invalidated_by=self.global_staff,
active=True,
)
certs_filter_args = {
"generated_certificate": self.generated_certificate,
"invalidated_by": self.global_staff,
"active": True
}
cert_invalidation_entry = certs_api.get_certificate_invalidation_entry(**certs_filter_args)
if cert_invalidation_entry is None:
raise ObjectDoesNotExist
def test_remove_certificate_invalidation_error(self):
"""

View File

@@ -39,12 +39,6 @@ from lms.djangoapps.bulk_email.api import is_bulk_email_feature_enabled
from lms.djangoapps.bulk_email.models_api import is_bulk_email_disabled_for_course
from lms.djangoapps.certificates import api as certs_api
from lms.djangoapps.certificates.data import CertificateStatuses
from lms.djangoapps.certificates.models import (
CertificateGenerationConfiguration,
CertificateGenerationHistory,
CertificateInvalidation,
GeneratedCertificate
)
from lms.djangoapps.courseware.access import has_access
from lms.djangoapps.courseware.block_render import get_block_by_usage_id
from lms.djangoapps.courseware.courses import get_studio_url
@@ -213,7 +207,7 @@ def instructor_dashboard_2(request, course_id): # lint-amnesty, pylint: disable
# This is used to generate example certificates
# and enable self-generated certificates for a course.
# Note: This is hidden for all CCXs
certs_enabled = CertificateGenerationConfiguration.current().enabled and not hasattr(course_key, 'ccx')
certs_enabled = certs_api.is_certificate_generation_enabled() and not hasattr(course_key, 'ccx')
certs_instructor_enabled = settings.FEATURES.get('ENABLE_CERTIFICATES_INSTRUCTOR_MANAGE', False)
if certs_enabled and (access['admin'] or (access['instructor'] and certs_instructor_enabled)):
@@ -250,7 +244,7 @@ def instructor_dashboard_2(request, course_id): # lint-amnesty, pylint: disable
kwargs={'course_id': str(course_key)}
)
certificate_invalidations = CertificateInvalidation.get_certificate_invalidations(course_key)
certificate_invalidations = certs_api.get_cert_invalidations_for_course(course_key)
context = {
'course': course,
@@ -375,7 +369,7 @@ def _section_certificates(course):
instructor_generation_enabled = settings.FEATURES.get('CERTIFICATES_INSTRUCTOR_GENERATION', False)
certificate_statuses_with_count = {
certificate['status']: certificate['count']
for certificate in GeneratedCertificate.get_unique_statuses(course_key=course.id)
for certificate in certs_api.get_unique_certificate_statuses(course.id)
}
return {
@@ -391,7 +385,7 @@ def _section_certificates(course):
'certificate_statuses_with_count': certificate_statuses_with_count,
'status': CertificateStatuses,
'certificate_generation_history':
CertificateGenerationHistory.objects.filter(course_id=course.id).order_by("-created"),
certs_api.get_cert_history_for_course_id(course_id=course.id).order_by("-created"),
'urls': {
'enable_certificate_generation': reverse(
'certificate_task',

View File

@@ -6,7 +6,7 @@ from django.core.exceptions import ValidationError
from django.utils.translation import gettext as _
from rest_framework import serializers
from lms.djangoapps.certificates.models import CertificateStatuses
from lms.djangoapps.certificates.data import CertificateStatuses
from lms.djangoapps.instructor.access import ROLES
from openedx.core.djangoapps.django_comment_common.models import (
FORUM_ROLE_ADMINISTRATOR,

View File

@@ -20,7 +20,7 @@ from opaque_keys.edx.keys import CourseKey, UsageKey
import xmodule.graders as xmgraders
from common.djangoapps.student.models import CourseEnrollment, CourseEnrollmentAllowed
from lms.djangoapps.certificates.data import CertificateStatuses
from lms.djangoapps.certificates.models import GeneratedCertificate
from lms.djangoapps.certificates.api import get_certificates_by_course_and_status
from lms.djangoapps.courseware.models import StudentModule
from lms.djangoapps.grades.api import context as grades_context
from lms.djangoapps.program_enrollments.api import fetch_program_enrollments_by_students
@@ -71,10 +71,13 @@ def issued_certificates(course_key, features):
report_run_date = datetime.date.today().strftime("%B %d, %Y")
certificate_features = [x for x in CERTIFICATE_FEATURES if x in features]
generated_certificates = list(GeneratedCertificate.eligible_certificates.filter(
course_id=course_key,
status=CertificateStatuses.downloadable
).values(*certificate_features).annotate(total_issued_certificate=Count('mode')))
generated_certificates = list(
get_certificates_by_course_and_status(
course_id=course_key,
status=CertificateStatuses.downloadable
).values(
*certificate_features).annotate(total_issued_certificate=Count('mode'))
)
# Report run date
for data in generated_certificates:

View File

@@ -20,6 +20,7 @@ from lms.djangoapps.instructor_analytics.basic import ( # lint-amnesty, pylint:
PROGRAM_ENROLLMENT_FEATURES,
STUDENT_FEATURES,
StudentModule,
issued_certificates,
enrolled_students_features,
get_available_features,
get_proctored_exam_results,
@@ -29,6 +30,7 @@ from lms.djangoapps.instructor_analytics.basic import ( # lint-amnesty, pylint:
list_problem_responses
)
from lms.djangoapps.program_enrollments.tests.factories import ProgramEnrollmentFactory
from lms.djangoapps.certificates.api import create_generated_certificate
from openedx.core.djangoapps.course_groups.tests.helpers import CohortFactory
from openedx.core.djangoapps.site_configuration.tests.factories import SiteConfigurationFactory
from common.djangoapps.student.models import CourseEnrollment, CourseEnrollmentAllowed
@@ -132,6 +134,40 @@ class TestAnalyticsBasic(ModuleStoreTestCase):
assert list(userreport.keys()) == ['username']
assert userreport['username'] in [user.username for user in self.users]
def test_issued_certificates_basic(self):
"""
Test that the `issued_certificates` function returns the correct aggregated data
for a single downloadable certificate.
This test:
- Creates a downloadable certificate for a user.
- Verifies that the function returns a list with a single item.
- Confirms that the returned certificate contains the expected course ID.
- Ensures that the total count of issued certificates is correct.
- Verifies that the 'report_run_date' field is present in the result.
The test ensures that the `issued_certificates` function behaves as expected
for a single downloadable certificate scenario.
"""
cert_args = {
"user": UserFactory(),
"course_id": self.course_key,
"mode": "honor",
"status": "downloadable",
"grade": "Pass"
}
create_generated_certificate(cert_args)
features = ['course_id', 'mode', 'status', 'grade']
results = issued_certificates(self.course_key, features)
assert isinstance(results, list)
assert len(results) == 1
cert = results[0]
assert cert['course_id'] == str(self.course_key)
assert cert['total_issued_certificate'] == 1
assert 'report_run_date' in cert
def test_enrolled_students_features_keys(self):
query_features = ('username', 'name', 'email', 'city', 'country',)
for user in self.users:

View File

@@ -16,7 +16,7 @@ from celery.states import READY_STATES
from common.djangoapps.util import milestones_helpers
from lms.djangoapps.bulk_email.api import get_course_email
from lms.djangoapps.certificates.models import CertificateGenerationHistory
from lms.djangoapps.certificates.api import create_or_update_certificate_generation_history
from lms.djangoapps.instructor_task.api_helper import (
QueueConnectionError,
check_arguments_for_overriding,
@@ -540,12 +540,14 @@ def generate_certificates_for_students(request, course_key, student_set=None, sp
task_key = ""
instructor_task = submit_task(request, task_type, task_class, course_key, task_input, task_key)
CertificateGenerationHistory.objects.create(
course_id=course_key,
generated_by=request.user,
instructor_task=instructor_task,
is_regeneration=False
)
cert_filter_args = {
"course_id": course_key,
"generated_by": request.user,
"instructor_task": instructor_task,
"is_regeneration": False
}
create_or_update_certificate_generation_history(**cert_filter_args)
return instructor_task
@@ -567,12 +569,14 @@ def regenerate_certificates(request, course_key, statuses_to_regenerate):
instructor_task = submit_task(request, task_type, task_class, course_key, task_input, task_key)
CertificateGenerationHistory.objects.create(
course_id=course_key,
generated_by=request.user,
instructor_task=instructor_task,
is_regeneration=True
)
cert_filter_args = {
"course_id": course_key,
"generated_by": request.user,
"instructor_task": instructor_task,
"is_regeneration": True
}
create_or_update_certificate_generation_history(**cert_filter_args)
return instructor_task

View File

@@ -23,7 +23,7 @@ from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.student.models import CourseEnrollment
from common.djangoapps.student.roles import BulkRoleCache
from lms.djangoapps.certificates import api as certs_api
from lms.djangoapps.certificates.models import GeneratedCertificate
from lms.djangoapps.certificates.api import get_certificates_for_course_and_users
from lms.djangoapps.course_blocks.api import get_course_blocks
from lms.djangoapps.courseware.user_state_client import DjangoXBlockUserStateClient
from lms.djangoapps.grades.api import CourseGradeFactory
@@ -242,7 +242,7 @@ class _CertificateBulkContext:
self.certificates_by_user = {
certificate.user.id: certificate
for certificate in
GeneratedCertificate.objects.filter(course_id=context.course_id, user__in=users)
get_certificates_for_course_and_users(course_id=context.course_id, users=users)
}
@@ -280,6 +280,7 @@ class InMemoryReportMixin:
"""
Mixin for a file report that will generate file in memory and then upload to report store
"""
def _generate(self):
"""
Internal method for generating a grade report for the given context.
@@ -341,6 +342,7 @@ class TemporaryFileReportMixin:
"""
Mixin for a file report that will write rows iteratively to a TempFile
"""
def _generate(self):
"""
Generate a CSV containing all students' problem grades within a given `course_id`.
@@ -415,6 +417,7 @@ class GradeReportBase:
"""
Base class for grade reports (ProblemGradeReport and CourseGradeReport).
"""
def __init__(self, context):
self.context = context

View File

@@ -19,7 +19,7 @@ from common.test.utils import normalize_repr
from lms.djangoapps.bulk_email.api import create_course_email
from lms.djangoapps.bulk_email.data import BulkEmailTargetChoices
from lms.djangoapps.certificates.data import CertificateStatuses
from lms.djangoapps.certificates.models import CertificateGenerationHistory
from lms.djangoapps.certificates.api import get_certificate_generation_history
from lms.djangoapps.instructor_task.api import (
SpecificStudentIdMissingError,
generate_anonymous_ids,
@@ -417,12 +417,13 @@ class InstructorTaskCourseSubmitTest(TestReportMixin, InstructorTaskCourseTestCa
self.create_task_request(self.instructor),
self.course.id
)
certificate_generation_history = CertificateGenerationHistory.objects.filter(
course_id=self.course.id,
generated_by=self.instructor,
instructor_task=instructor_task,
is_regeneration=False
)
cert_args = {
"course_id": self.course.id,
"generated_by": self.instructor,
"instructor_task": instructor_task,
"is_regeneration": False
}
certificate_generation_history = get_certificate_generation_history(**cert_args)
# Validate that record was added to CertificateGenerationHistory
assert certificate_generation_history.exists()
@@ -432,12 +433,13 @@ class InstructorTaskCourseSubmitTest(TestReportMixin, InstructorTaskCourseTestCa
self.course.id,
[CertificateStatuses.downloadable, CertificateStatuses.generating]
)
certificate_generation_history = CertificateGenerationHistory.objects.filter(
course_id=self.course.id,
generated_by=self.instructor,
instructor_task=instructor_task,
is_regeneration=True
)
cert_args = {
"course_id": self.course.id,
"generated_by": self.instructor,
"instructor_task": instructor_task,
"is_regeneration": True
}
certificate_generation_history = get_certificate_generation_history(**cert_args)
# Validate that record was added to CertificateGenerationHistory
assert certificate_generation_history.exists()

View File

@@ -31,7 +31,6 @@ from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.student.models import CourseEnrollment, CourseEnrollmentAllowed
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory
from lms.djangoapps.certificates.data import CertificateStatuses
from lms.djangoapps.certificates.models import GeneratedCertificate
from lms.djangoapps.certificates.tests.factories import CertificateAllowlistFactory, GeneratedCertificateFactory
from lms.djangoapps.courseware.models import StudentModule
from lms.djangoapps.grades.course_data import CourseData
@@ -116,6 +115,7 @@ class TestInstructorGradeReport(InstructorGradeReportTestCase):
"""
Tests that CSV grade report generation works.
"""
def setUp(self):
super().setUp()
self.course = CourseFactory.create()
@@ -810,7 +810,7 @@ class TestProblemResponsesReport(TestReportMixin, InstructorTaskModuleTestCase):
'user_id': self.instructor.id
}
with patch('lms.djangoapps.instructor_task.tasks_helper.runner._get_current_task'), \
freeze_time('2020-01-01'):
freeze_time('2020-01-01'):
with patch('lms.djangoapps.instructor_task.tasks_helper.grades'
'.ProblemResponses._build_student_data') as mock_build_student_data:
mock_build_student_data.return_value = (
@@ -832,6 +832,7 @@ class TestProblemGradeReport(TestReportMixin, InstructorTaskModuleTestCase):
"""
Test that the problem CSV generation works.
"""
def setUp(self):
super().setUp()
self.initialize_course()
@@ -1138,6 +1139,7 @@ class TestProblemReportCohortedContent(TestReportMixin, ContentGroupTestCase, In
"""
Test the problem report on a course that has cohorted content.
"""
def setUp(self):
super().setUp()
# construct cohorted problems to work on.
@@ -1238,6 +1240,7 @@ class TestCourseSurveyReport(TestReportMixin, InstructorTaskCourseTestCase):
"""
Tests that Course Survey report generation works.
"""
def setUp(self):
super().setUp()
self.course = CourseFactory.create()
@@ -1333,6 +1336,7 @@ class TestStudentReport(TestReportMixin, InstructorTaskCourseTestCase):
"""
Tests that CSV student profile report generation works.
"""
def setUp(self):
super().setUp()
self.course = CourseFactory.create()
@@ -1475,6 +1479,7 @@ class TestListMayEnroll(TestReportMixin, InstructorTaskCourseTestCase):
students who may enroll in a given course (but have not signed up
for it yet) works.
"""
def _create_enrollment(self, email):
"""
Factory method for creating CourseEnrollmentAllowed objects.
@@ -1521,6 +1526,7 @@ class TestListMayEnroll(TestReportMixin, InstructorTaskCourseTestCase):
class MockDefaultStorage:
"""Mock django's DefaultStorage"""
def __init__(self):
pass
@@ -1534,6 +1540,7 @@ class TestCohortStudents(TestReportMixin, InstructorTaskCourseTestCase):
"""
Tests that bulk student cohorting works.
"""
def setUp(self):
super().setUp()
@@ -1797,6 +1804,7 @@ class TestGradeReport(TestReportMixin, InstructorTaskModuleTestCase):
"""
Test that grade report has correct grade values.
"""
def setUp(self):
super().setUp()
self.create_course()
@@ -1981,8 +1989,8 @@ class TestGradeReport(TestReportMixin, InstructorTaskModuleTestCase):
if create_non_zero_grade:
self.submit_student_answer(self.student.username, 'Problem1', ['Option 1'])
with patch('lms.djangoapps.instructor_task.tasks_helper.runner._get_current_task'), \
patch('lms.djangoapps.grades.course_data.get_course_blocks') as mock_course_blocks, \
patch('lms.djangoapps.grades.subsection_grade.get_score') as mock_get_score:
patch('lms.djangoapps.grades.course_data.get_course_blocks') as mock_course_blocks, \
patch('lms.djangoapps.grades.subsection_grade.get_score') as mock_get_score:
CourseGradeReport.generate(None, None, self.course.id, {}, 'graded')
assert not mock_course_blocks.called
assert not mock_get_score.called
@@ -1994,6 +2002,7 @@ class TestGradeReportEnrollmentAndCertificateInfo(TestReportMixin, InstructorTas
"""
Test that grade report has correct user enrollment, verification, and certificate information.
"""
def setUp(self):
super().setUp()
@@ -2147,7 +2156,7 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
user=student,
course_id=self.course.id,
status=CertificateStatuses.downloadable,
mode=GeneratedCertificate.CourseMode.VERIFIED
mode=CourseMode.VERIFIED
)
# Allowlist 5 students
@@ -2307,7 +2316,7 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
user=student,
course_id=self.course.id,
status=CertificateStatuses.downloadable,
mode=GeneratedCertificate.CourseMode.VERIFIED
mode=CourseMode.VERIFIED
)
# Grant error certs to 3 students
@@ -2316,7 +2325,7 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
user=student,
course_id=self.course.id,
status=CertificateStatuses.error,
mode=GeneratedCertificate.CourseMode.VERIFIED
mode=CourseMode.VERIFIED
)
# Grant a deleted cert to the 6th student
@@ -2325,7 +2334,7 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
user=student,
course_id=self.course.id,
status=CertificateStatuses.deleted,
mode=GeneratedCertificate.CourseMode.VERIFIED
mode=CourseMode.VERIFIED
)
# Allowlist 7 students
@@ -2367,7 +2376,7 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
user=student,
course_id=self.course.id,
status=CertificateStatuses.downloadable,
mode=GeneratedCertificate.CourseMode.VERIFIED,
mode=CourseMode.VERIFIED,
grade=default_grade
)
@@ -2377,7 +2386,7 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
user=student,
course_id=self.course.id,
status=CertificateStatuses.error,
mode=GeneratedCertificate.CourseMode.VERIFIED,
mode=CourseMode.VERIFIED,
grade=default_grade
)
@@ -2387,7 +2396,7 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
user=student,
course_id=self.course.id,
status=CertificateStatuses.deleted,
mode=GeneratedCertificate.CourseMode.VERIFIED,
mode=CourseMode.VERIFIED,
grade=default_grade
)
@@ -2397,7 +2406,7 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
user=student,
course_id=self.course.id,
status=CertificateStatuses.generating,
mode=GeneratedCertificate.CourseMode.VERIFIED,
mode=CourseMode.VERIFIED,
grade=default_grade
)
@@ -2438,7 +2447,7 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
user=student,
course_id=self.course.id,
status=CertificateStatuses.downloadable,
mode=GeneratedCertificate.CourseMode.VERIFIED,
mode=CourseMode.VERIFIED,
grade=default_grade
)
@@ -2448,7 +2457,7 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
user=student,
course_id=self.course.id,
status=CertificateStatuses.error,
mode=GeneratedCertificate.CourseMode.VERIFIED,
mode=CourseMode.VERIFIED,
grade=default_grade
)
@@ -2458,7 +2467,7 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
user=student,
course_id=self.course.id,
status=CertificateStatuses.unavailable,
mode=GeneratedCertificate.CourseMode.VERIFIED,
mode=CourseMode.VERIFIED,
grade=default_grade
)
@@ -2468,7 +2477,7 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
user=student,
course_id=self.course.id,
status=CertificateStatuses.generating,
mode=GeneratedCertificate.CourseMode.VERIFIED,
mode=CourseMode.VERIFIED,
grade=default_grade
)
@@ -2513,7 +2522,7 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
user=student,
course_id=self.course.id,
status=CertificateStatuses.downloadable,
mode=GeneratedCertificate.CourseMode.VERIFIED
mode=CourseMode.VERIFIED
)
# Grant error certs to 3 students
@@ -2522,7 +2531,7 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
user=student,
course_id=self.course.id,
status=CertificateStatuses.error,
mode=GeneratedCertificate.CourseMode.VERIFIED
mode=CourseMode.VERIFIED
)
# Grant a deleted cert to the 6th student
@@ -2531,7 +2540,7 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
user=student,
course_id=self.course.id,
status=CertificateStatuses.deleted,
mode=GeneratedCertificate.CourseMode.VERIFIED
mode=CourseMode.VERIFIED
)
# Grant a notpassing cert to the 7th student
@@ -2540,7 +2549,7 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
user=student,
course_id=self.course.id,
status=CertificateStatuses.notpassing,
mode=GeneratedCertificate.CourseMode.VERIFIED
mode=CourseMode.VERIFIED
)
# Allowlist 7 students

View File

@@ -28,8 +28,7 @@ from xmodule.x_module import PUBLIC_VIEW, STUDENT_VIEW
from common.djangoapps.course_modes.models import CourseMode, get_course_prices
from common.djangoapps.util.views import expose_header
from lms.djangoapps.edxnotes.helpers import is_feature_enabled
from lms.djangoapps.certificates.api import get_certificate_url
from lms.djangoapps.certificates.models import GeneratedCertificate
from lms.djangoapps.certificates.api import get_certificate_url, get_eligible_certificate
from lms.djangoapps.commerce.utils import EcommerceService
from lms.djangoapps.course_api.api import course_detail
from lms.djangoapps.course_goals.models import UserActivity
@@ -279,12 +278,12 @@ class CoursewareMeta:
linkedin_config = LinkedInAddToProfileConfiguration.current()
if linkedin_config.is_enabled():
try:
user_certificate = GeneratedCertificate.eligible_certificates.get(
user=self.effective_user, course_id=self.course_key
)
except GeneratedCertificate.DoesNotExist:
user_certificate = get_eligible_certificate(user=self.effective_user, course_id=self.course_key)
if user_certificate is None:
return
cert_url = self.request.build_absolute_uri(
get_certificate_url(course_id=self.course_key, uuid=user_certificate.verify_uuid)
)

View File

@@ -16,9 +16,11 @@ from MySQLdb import OperationalError
from opaque_keys.edx.keys import CourseKey
from common.djangoapps.course_modes.models import CourseMode
from lms.djangoapps.certificates.api import get_recently_modified_certificates
from lms.djangoapps.certificates.api import (
get_generated_certificate,
get_recently_modified_certificates
)
from lms.djangoapps.certificates.data import CertificateStatuses
from lms.djangoapps.certificates.models import GeneratedCertificate
from lms.djangoapps.grades.api import CourseGradeFactory, get_recently_modified_grades
from openedx.core.djangoapps.catalog.utils import get_programs
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
@@ -352,16 +354,15 @@ def send_grade_if_interesting(
# If we don't have mode and/or status, retrieve them from the learner's certificate record
if mode is None or status is None:
try:
cert = GeneratedCertificate.objects.get(user=user, course_id=course_run_key) # pylint: disable=no-member
mode = cert.mode
status = cert.status
except GeneratedCertificate.DoesNotExist:
# we only care about grades for which there is a certificate record
cert = get_generated_certificate(user=user, course_id=course_run_key)
if cert is None:
if verbose:
logger.warning(f"{warning_base} no certificate record in the specified course run")
return
mode = cert.mode
status = cert.status
# Don't worry about the certificate record being in a passing or awarded status. Having a certificate record in any
# status is good enough to record a verified attempt at a course. The Credentials IDA keeps track of how many times
# a learner has made an attempt at a course run of a course, so it wants to know about all the learner's efforts.

View File

@@ -19,7 +19,6 @@ from testfixtures import LogCapture
from common.djangoapps.student.tests.factories import UserFactory
from lms.djangoapps.certificates.api import get_recently_modified_certificates
from lms.djangoapps.certificates.data import CertificateStatuses
from lms.djangoapps.certificates.models import GeneratedCertificate
from lms.djangoapps.grades.models import PersistentCourseGrade
from lms.djangoapps.grades.models_api import get_recently_modified_grades
from lms.djangoapps.grades.tests.utils import mock_passing_grade
@@ -189,7 +188,7 @@ class TestHandleNotifyCredentialsTask(TestCase):
grade2 = PersistentCourseGrade.objects.create(user_id=self.user.id, course_id='course-v1:edX+Test+22',
percent_grade=1)
total_certificates = GeneratedCertificate.objects.filter(**cert_filter_args).order_by('modified_date') # pylint: disable=no-member
total_certificates = get_recently_modified_certificates(**cert_filter_args)
total_grades = PersistentCourseGrade.objects.all()
self.options['auto'] = True

View File

@@ -19,7 +19,8 @@ from opaque_keys.edx.keys import CourseKey
from requests.exceptions import HTTPError
from common.djangoapps.course_modes.models import CourseMode
from lms.djangoapps.certificates.models import GeneratedCertificate
from lms.djangoapps.certificates.data import GeneratedCertificateData
from lms.djangoapps.certificates.api import get_eligible_certificate
from openedx.core.djangoapps.content.course_overviews.api import get_course_overview_or_none
from openedx.core.djangoapps.credentials.api import is_credentials_enabled
from openedx.core.djangoapps.credentials.utils import (
@@ -195,7 +196,7 @@ def revoke_program_certificate(client, username, program_uuid):
def post_course_certificate(
client: "Session",
username: str,
certificate: GeneratedCertificate,
certificate: GeneratedCertificateData,
date_override: Optional["datetime"] = None,
org: Optional[str] = None,
):
@@ -517,12 +518,9 @@ def award_course_certificate(self, username, course_run_key):
return
# Get the cert for the course key and username if it's both passing and available in professional/verified
try:
certificate = GeneratedCertificate.eligible_certificates.get(
user=user.id,
course_id=course_key,
)
except GeneratedCertificate.DoesNotExist:
certificate = get_eligible_certificate(user=user, course_id=course_key)
if certificate is None:
LOGGER.warning(
f"Task award_course_certificate was called for user {user.id} in course run {course_key} but this learner "
"has not earned a course certificate in this course run"

View File

@@ -25,7 +25,6 @@ from common.djangoapps.student.models import CourseEnrollment
from common.djangoapps.util.date_utils import strftime_localized
from lms.djangoapps.certificates import api as certificate_api
from lms.djangoapps.certificates.data import CertificateStatuses
from lms.djangoapps.certificates.models import GeneratedCertificate
from lms.djangoapps.commerce.utils import EcommerceService, get_program_price_info
from openedx.core.djangoapps.catalog.api import get_programs_by_type
from openedx.core.djangoapps.catalog.constants import PathwayType
@@ -341,7 +340,7 @@ class ProgramProgressMeter:
Returns a dict of {uuid_string: available_datetime}
"""
# Query for all user certs up front, for performance reasons (rather than querying per course run).
user_certificates = GeneratedCertificate.eligible_available_certificates.filter(user=self.user)
user_certificates = certificate_api.get_eligible_and_available_certificates(user=self.user)
certificates_by_run = {cert.course_id: cert for cert in user_certificates}
completed = {}