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:
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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, [])
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
Reference in New Issue
Block a user