Skip to content

Commit cd6faeb

Browse files
dwong2708wgu-ram-chandradeborahgu
authored
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 <[email protected]> Co-authored-by: Deborah Kaplan <[email protected]>
1 parent 7e72ec2 commit cd6faeb

File tree

20 files changed

+621
-168
lines changed

20 files changed

+621
-168
lines changed

common/djangoapps/student/models/course_enrollment.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -179,13 +179,10 @@ def expired(self, username, time_zone=UTC):
179179

180180
def get_user_course_ids_with_certificates(self, username):
181181
"""
182-
Gets user's course ids with certificates.
182+
Retrieve the list of course IDs for which the given user has earned certificates.
183183
"""
184-
from lms.djangoapps.certificates.models import GeneratedCertificate # pylint: disable=import-outside-toplevel
185-
course_ids_with_certificates = GeneratedCertificate.objects.filter(
186-
user__username=username
187-
).values_list('course_id', flat=True)
188-
return course_ids_with_certificates
184+
from lms.djangoapps.certificates.api import get_course_ids_from_certs_for_user
185+
return get_course_ids_from_certs_for_user(username)
189186

190187

191188
class CourseEnrollmentManager(models.Manager):

lms/djangoapps/certificates/api.py

Lines changed: 322 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,15 @@
2424
from common.djangoapps.student.models import CourseEnrollment
2525
from lms.djangoapps.branding import api as branding_api
2626
from lms.djangoapps.certificates.config import AUTO_CERTIFICATE_GENERATION as _AUTO_CERTIFICATE_GENERATION
27-
from lms.djangoapps.certificates.data import CertificateStatuses
27+
from lms.djangoapps.certificates.data import CertificateStatuses, GeneratedCertificateData
2828
from lms.djangoapps.certificates.generation_handler import generate_certificate_task as _generate_certificate_task
2929
from lms.djangoapps.certificates.generation_handler import is_on_certificate_allowlist as _is_on_certificate_allowlist
3030
from lms.djangoapps.certificates.models import (
3131
CertificateAllowlist,
3232
CertificateDateOverride,
3333
CertificateGenerationConfiguration,
3434
CertificateGenerationCourseSetting,
35+
CertificateGenerationHistory,
3536
CertificateInvalidation,
3637
CertificateTemplate,
3738
CertificateTemplateAsset,
@@ -129,19 +130,16 @@ def get_certificate_for_user(username, course_key, format_results=True):
129130
Arguments:
130131
username (unicode): The identifier of the user.
131132
course_key (CourseKey): A Course Key.
133+
format_results (boolean): Default True. If False, return the GeneratedCertificate object
134+
instead of the serialized dict representation.
132135
Returns:
133-
A dict containing information about the certificate or, optionally,
136+
A dict containing a serialized representation of the certificate or, optionally,
134137
the GeneratedCertificate object itself.
138+
If there is no GeneratedCertificate object for the user and course combination, returns None.
135139
"""
136-
try:
137-
cert = GeneratedCertificate.eligible_certificates.get(user__username=username, course_id=course_key)
138-
except GeneratedCertificate.DoesNotExist:
139-
return None
140140

141-
if format_results:
142-
return _format_certificate_for_user(username, cert)
143-
else:
144-
return cert
141+
cert = GeneratedCertificate.eligible_certificates.filter(user__username=username, course_id=course_key).first()
142+
return _format_certificate_for_user(username, cert) if cert and format_results else cert
145143

146144

147145
def get_certificate_for_user_id(user, course_id):
@@ -728,16 +726,44 @@ def create_certificate_invalidation_entry(certificate, user_requesting_invalidat
728726
return certificate_invalidation
729727

730728

731-
def get_certificate_invalidation_entry(certificate):
729+
def get_certificate_invalidation_entry(generated_certificate, invalidated_by=None, notes=None, active=None):
732730
"""
733-
Retrieves and returns an certificate invalidation entry for a given certificate id.
731+
Retrieve a certificate invalidation entry for a specified certificate.
732+
733+
Optionally filter the invalidation entry by who invalidated it, notes, and whether the invalidation is active.
734+
735+
Args:
736+
generated_certificate (GeneratedCertificate): The certificate to find the invalidation entry for.
737+
invalidated_by (User, optional): User who invalidated the certificate. Defaults to None.
738+
notes (str, optional): Notes associated with the invalidation. Defaults to None.
739+
active (bool, optional): Whether the invalidation entry is currently active. Defaults to None.
740+
741+
Returns:
742+
CertificateInvalidationEntry or None: The matching invalidation entry if found, else None.
734743
"""
735-
log.info(f"Attempting to retrieve certificate invalidation entry for certificate with id {certificate.id}.")
736-
try:
737-
certificate_invalidation_entry = CertificateInvalidation.objects.get(generated_certificate=certificate)
738-
except ObjectDoesNotExist:
739-
log.warning(f"No certificate invalidation found linked to certificate with id {certificate.id}.")
740-
return None
744+
log.info(
745+
f"Attempting to retrieve certificate invalidation entry "
746+
f"for certificate with id {generated_certificate.id}."
747+
)
748+
749+
cert_filter_args = {
750+
"generated_certificate": generated_certificate
751+
}
752+
753+
if invalidated_by is not None:
754+
cert_filter_args["invalidated_by"] = invalidated_by
755+
756+
if notes is not None:
757+
cert_filter_args["notes"] = notes
758+
759+
if active is not None:
760+
cert_filter_args["active"] = active
761+
762+
certificate_invalidation_entry = CertificateInvalidation.objects.filter(**cert_filter_args).first()
763+
764+
# If object does not exist, add a warning to the logs
765+
if certificate_invalidation_entry is None:
766+
log.warning(f"No certificate invalidation found linked to certificate with id {generated_certificate.id}.")
741767

742768
return certificate_invalidation_entry
743769

@@ -958,3 +984,281 @@ def clear_pii_from_certificate_records_for_user(user):
958984
None
959985
"""
960986
GeneratedCertificate.objects.filter(user=user).update(name="")
987+
988+
989+
def get_cert_history_for_course_id(course_id):
990+
"""
991+
Retrieve all certificate generation history records for the specified course.
992+
993+
Args:
994+
course_id (CourseLocator | CourseKey): The unique identifier for the course.
995+
996+
Returns:
997+
QuerySet[CertificateGenerationHistory]: A queryset of all certificate generation history records
998+
associated with the specified course.
999+
"""
1000+
return CertificateGenerationHistory.objects.filter(course_id=course_id)
1001+
1002+
1003+
def is_certificate_generation_enabled():
1004+
"""
1005+
Checks if certificate generation is currently enabled.
1006+
1007+
This function queries the `CertificateGenerationConfiguration` model to retrieve the
1008+
current configuration and returns whether certificate generation is enabled or not.
1009+
1010+
Returns:
1011+
bool: True if certificate generation is enabled, False otherwise.
1012+
"""
1013+
return CertificateGenerationConfiguration.current().enabled
1014+
1015+
1016+
def set_certificate_generation_config(enabled=True):
1017+
"""
1018+
Configures the certificate generation settings.
1019+
1020+
Args:
1021+
enabled (bool): If True, enables certificate generation; if False, disables it. Default is True.
1022+
1023+
Returns:
1024+
CertificateGenerationConfiguration: The created or updated certificate generation configuration object.
1025+
"""
1026+
return CertificateGenerationConfiguration.objects.create(enabled=enabled)
1027+
1028+
1029+
def get_certificate_generation_history(course_id=None, generated_by=None, instructor_task=None, is_regeneration=None):
1030+
"""
1031+
Retrieves a queryset of `CertificateGenerationHistory` records, filtered by the provided criteria.
1032+
1033+
Args:
1034+
course_id (Optional[CourseKey]): The unique ID of the course (if filtering by course).
1035+
generated_by (Optional[User]): The user who generated the certificate (if filtering by user).
1036+
instructor_task (Optional[bool]): Whether the certificate generation was triggered by an instructor task.
1037+
is_regeneration (Optional[bool]): Whether the certificate was regenerated.
1038+
1039+
Returns:
1040+
QuerySet[CertificateGenerationHistory]: A queryset of filtered `CertificateGenerationHistory` records.
1041+
"""
1042+
cert_filter_args = {}
1043+
1044+
# Only add the filter if the corresponding argument is provided
1045+
if course_id is not None:
1046+
cert_filter_args["course_id"] = course_id
1047+
1048+
if generated_by is not None:
1049+
cert_filter_args["generated_by"] = generated_by
1050+
1051+
if instructor_task is not None:
1052+
cert_filter_args["instructor_task"] = instructor_task
1053+
1054+
if is_regeneration is not None:
1055+
cert_filter_args["is_regeneration"] = is_regeneration
1056+
1057+
res = CertificateGenerationHistory.objects.filter(**cert_filter_args)
1058+
1059+
return res
1060+
1061+
1062+
def create_or_update_certificate_generation_history(course_id, generated_by, instructor_task, is_regeneration):
1063+
"""
1064+
Creates a new certificate generation history record or updates an existing one.
1065+
1066+
Args:
1067+
course_id (CourseKey): The unique identifier for the course run.
1068+
generated_by (User): The user (typically an instructor or admin) who initiated the certificate generation.
1069+
instructor_task (str): A descriptor or task identifier for the instructor-related task.
1070+
is_regeneration (bool): A flag indicating whether the certificate is being
1071+
regenerated (True) or newly generated (False).
1072+
1073+
Returns:
1074+
CertificateGenerationHistory: The created or updated CertificateGenerationHistory instance.
1075+
"""
1076+
cert_history, created = CertificateGenerationHistory.objects.update_or_create(
1077+
course_id=course_id,
1078+
generated_by=generated_by,
1079+
instructor_task=instructor_task,
1080+
is_regeneration=is_regeneration
1081+
)
1082+
1083+
return cert_history
1084+
1085+
1086+
def create_or_update_eligible_certificate_for_user(user, course_id, status):
1087+
"""
1088+
Create or update an eligible GeneratedCertificate for a user in a specific course.
1089+
1090+
Args:
1091+
user (User): The user for whom the certificate is being created or updated.
1092+
course_id (CourseKey): The unique identifier for the course the certificate applies to.
1093+
status (str): The status of the certificate (e.g., "downloadable", "issued").
1094+
1095+
Returns:
1096+
tuple: A tuple containing:
1097+
- `GeneratedCertificate`: The created or updated certificate.
1098+
- `bool`: A boolean indicating whether the certificate was created (True) or updated (False).
1099+
"""
1100+
1101+
cert, created = GeneratedCertificate.eligible_certificates.update_or_create(
1102+
user=user,
1103+
course_id=course_id,
1104+
status=status
1105+
)
1106+
1107+
return cert, created
1108+
1109+
1110+
def get_cert_invalidations_for_course(course_key):
1111+
"""
1112+
Retrieves all certificate invalidations associated with the specified course.
1113+
1114+
Args:
1115+
course_key (CourseKey): The unique identifier for the course run.
1116+
1117+
Returns:
1118+
QuerySet[CertificateInvalidation]: A queryset containing all invalidations for the specified course.
1119+
"""
1120+
return CertificateInvalidation.get_certificate_invalidations(course_key)
1121+
1122+
1123+
def get_certificates_for_course_and_users(course_id, users):
1124+
"""
1125+
Retrieves all GeneratedCertificate records for a specific course and a list of users.
1126+
1127+
Args:
1128+
course_id (CourseKey): The unique identifier for the course run.
1129+
users (Iterable[User]): A list or queryset of User instances to filter certificates by.
1130+
1131+
Returns:
1132+
QuerySet[GeneratedCertificate]: A queryset containing the matching certificate records.
1133+
"""
1134+
return GeneratedCertificate.objects.filter(course_id=course_id, user__in=users)
1135+
1136+
1137+
def get_course_ids_from_certs_for_user(username):
1138+
"""
1139+
Retrieves a list of course IDs for which the given user has generated certificates.
1140+
1141+
Args:
1142+
username (str): The username of the user whose course IDs are being retrieved.
1143+
1144+
Returns:
1145+
list: A list of course IDs for which the user has generated certificates.
1146+
If no certificates are found, an empty list is returned.
1147+
"""
1148+
course_ids_with_certificates = GeneratedCertificate.objects.filter(
1149+
user__username=username
1150+
).values_list('course_id', flat=True)
1151+
1152+
return list(course_ids_with_certificates)
1153+
1154+
1155+
def get_generated_certificate(user, course_id):
1156+
"""
1157+
Retrieves the GeneratedCertificateData for the given user and course.
1158+
1159+
Args:
1160+
user (User): The user for whom the certificate is being fetched.
1161+
course_id (CourseKey): The unique identifier for the course.
1162+
1163+
Returns:
1164+
Optional[GeneratedCertificateData]: A `GeneratedCertificateData` object if found, otherwise None.
1165+
"""
1166+
try:
1167+
cert = GeneratedCertificate.objects.get(user=user, course_id=course_id)
1168+
return GeneratedCertificateData(
1169+
user=cert.user,
1170+
course_id=cert.course_id
1171+
)
1172+
except GeneratedCertificate.DoesNotExist:
1173+
return None
1174+
1175+
1176+
def create_generated_certificate(cert_args):
1177+
"""
1178+
Creates a new `GeneratedCertificate` object using the provided arguments.
1179+
1180+
Args:
1181+
cert_args (dict): A dictionary containing the certificate's attributes, such as
1182+
"user", "course_id", "status", etc.
1183+
1184+
Returns:
1185+
GeneratedCertificate: The newly created `GeneratedCertificate` object.
1186+
1187+
Raises:
1188+
ValueError: If any required fields ("user", "course_id") are missing from `cert_args`.
1189+
"""
1190+
1191+
required_fields = {"user", "course_id"}
1192+
1193+
# Check if all required fields are present
1194+
if not all(field in cert_args for field in required_fields):
1195+
raise ValueError(f"Missing required fields: {required_fields - cert_args.keys()}")
1196+
1197+
# Create and return the GeneratedCertificate object
1198+
return GeneratedCertificate.objects.create(**cert_args)
1199+
1200+
1201+
def get_eligible_certificate(user, course_id):
1202+
"""
1203+
Retrieves the eligible GeneratedCertificate for a given user and course.
1204+
1205+
Args:
1206+
user (User): The user object for whom the certificate is being retrieved.
1207+
course_id (CourseKey): The unique identifier for the course run.
1208+
1209+
Returns:
1210+
GeneratedCertificate or None: The eligible certificate instance if one exists; otherwise, None.
1211+
"""
1212+
try:
1213+
return GeneratedCertificate.eligible_certificates.get(
1214+
user=user.id,
1215+
course_id=course_id
1216+
)
1217+
except GeneratedCertificate.DoesNotExist:
1218+
return None
1219+
1220+
1221+
def get_eligible_and_available_certificates(user):
1222+
"""
1223+
Retrieves all eligible and available certificates for the specified user.
1224+
1225+
Args:
1226+
user (User): The user whose eligible and available certificates are being retrieved.
1227+
1228+
Returns:
1229+
QuerySet[GeneratedCertificate]: A queryset containing all eligible and available certificates
1230+
for the specified user.
1231+
"""
1232+
return GeneratedCertificate.eligible_available_certificates.filter(user=user)
1233+
1234+
1235+
def get_certificates_by_course_and_status(course_id, status):
1236+
"""
1237+
Retrieves all eligible certificates for a specific course and status.
1238+
1239+
Args:
1240+
course_id (CourseKey): The unique identifier for the course run.
1241+
status (str): The status of the certificates to filter by (e.g., "downloadable", "issued").
1242+
1243+
Returns:
1244+
QuerySet[GeneratedCertificate]: A queryset containing the eligible certificates
1245+
matching the provided course ID and status.
1246+
"""
1247+
1248+
return GeneratedCertificate.eligible_certificates.filter(
1249+
course_id=course_id,
1250+
status=status
1251+
)
1252+
1253+
1254+
def get_unique_certificate_statuses(course_key):
1255+
"""
1256+
Retrieves the unique certificate statuses for the specified course.
1257+
1258+
Args:
1259+
course_key (CourseKey): The unique identifier for the course run.
1260+
1261+
Returns:
1262+
QuerySet[str]: A queryset containing the unique certificate statuses for the specified course.
1263+
"""
1264+
return GeneratedCertificate.get_unique_statuses(course_key=course_key)

0 commit comments

Comments
 (0)