Skip to content

Commit 6f7d9ce

Browse files
fix: add audit trail on first enrollment as well (#3589)
1 parent f0e7b68 commit 6f7d9ce

3 files changed

Lines changed: 85 additions & 2 deletions

File tree

courses/api.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,8 @@ def create_local_enrollment(user, run, *, mode=EDX_DEFAULT_ENROLLMENT_MODE):
159159
"enrollment_mode": mode,
160160
},
161161
)
162+
if created:
163+
enrollment.save_and_log(None)
162164
if not created and not enrollment.active:
163165
enrollment.reactivate_and_save()
164166
if not enrollment.edx_enrolled:
@@ -249,6 +251,9 @@ def send_enrollment_emails():
249251
),
250252
)
251253

254+
if created:
255+
enrollment.save_and_log(None)
256+
252257
# If the run is associated with a B2B contract, add the contract
253258
# to the user's contract list and update their org memberships
254259
if run.b2b_contract:
@@ -323,6 +328,9 @@ def create_program_enrollments(
323328
"enrollment_mode": enrollment_mode,
324329
},
325330
)
331+
if created:
332+
enrollment.save_and_log(None)
333+
326334
if not created and enrollment.change_status is not None:
327335
enrollment.reactivate_and_save()
328336

courses/api_test.py

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,12 @@
8484
from courses.models import (
8585
CourseRunCertificate,
8686
CourseRunEnrollment,
87+
CourseRunEnrollmentAudit,
8788
EnrollmentMode,
8889
PaidCourseRun,
8990
ProgramCertificate,
9091
ProgramEnrollment,
92+
ProgramEnrollmentAudit,
9193
ProgramRequirement,
9294
ProgramRequirementNodeType,
9395
)
@@ -243,6 +245,26 @@ def test_create_local_enrollment_existing_enrollment(
243245
assert enrollment.edx_enrolled is True
244246

245247

248+
def test_create_local_enrollment_writes_audit_on_creation(user):
249+
"""
250+
create_local_enrollment should write a CourseRunEnrollmentAudit row when a
251+
brand-new enrollment is created. Without this, the very first event in an
252+
enrollment's lifecycle would be missing from the audit table.
253+
"""
254+
run = CourseRunFactory.create()
255+
256+
enrollment, created = create_local_enrollment(user, run)
257+
258+
assert created is True
259+
audit_rows = CourseRunEnrollmentAudit.objects.filter(enrollment=enrollment)
260+
assert audit_rows.count() == 1
261+
audit_row = audit_rows.first()
262+
assert audit_row.acting_user is None
263+
assert audit_row.data_after["id"] == enrollment.id
264+
assert audit_row.data_after["active"] is True
265+
assert audit_row.data_after["edx_enrolled"] is True
266+
267+
246268
@pytest.mark.parametrize(
247269
"enrollment_mode", [EDX_DEFAULT_ENROLLMENT_MODE, EDX_ENROLLMENT_VERIFIED_MODE]
248270
)
@@ -295,6 +317,32 @@ def test_create_run_enrollments(
295317
patched_send_enrollment_email.assert_any_call(enrollment)
296318

297319

320+
def test_create_run_enrollments_writes_audit_on_creation(mocker, user):
321+
"""
322+
create_run_enrollments should write a CourseRunEnrollmentAudit row for every
323+
newly-created enrollment.
324+
"""
325+
runs = CourseRunFactory.create_batch(2)
326+
mocker.patch("courses.api.enroll_in_edx_course_runs")
327+
mocker.patch("courses.api.mail_api.send_course_run_enrollment_email")
328+
mocker.patch("courses.tasks.subscribe_edx_course_emails.delay")
329+
330+
successful_enrollments, edx_request_success = create_run_enrollments(user, runs)
331+
332+
assert edx_request_success is True
333+
assert len(successful_enrollments) == len(runs)
334+
for enrollment in successful_enrollments:
335+
audit_rows = CourseRunEnrollmentAudit.objects.filter(enrollment=enrollment)
336+
assert audit_rows.count() == 1, (
337+
f"expected exactly one audit row for the newly-created enrollment {enrollment.id}, "
338+
f"found {audit_rows.count()}"
339+
)
340+
audit_row = audit_rows.first()
341+
assert audit_row.acting_user is None
342+
assert audit_row.data_after["id"] == enrollment.id
343+
assert audit_row.data_after["active"] is True
344+
345+
298346
@pytest.mark.parametrize("is_active", [True, False])
299347
def test_create_run_enrollments_upgrade(
300348
mocker,
@@ -526,6 +574,28 @@ def test_create_program_enrollments_creation(user, mode):
526574
assert enrollment.enrollment_mode == mode
527575

528576

577+
def test_create_program_enrollments_writes_audit_on_creation(user):
578+
"""
579+
create_program_enrollments should write a ProgramEnrollmentAudit row for
580+
every newly-created enrollment.
581+
"""
582+
programs = ProgramFactory.create_batch(2)
583+
584+
successful_enrollments = create_program_enrollments(user, programs)
585+
586+
assert len(successful_enrollments) == len(programs)
587+
for enrollment in successful_enrollments:
588+
audit_rows = ProgramEnrollmentAudit.objects.filter(enrollment=enrollment)
589+
assert audit_rows.count() == 1, (
590+
f"expected exactly one audit row for the newly-created enrollment {enrollment.id}, "
591+
f"found {audit_rows.count()}"
592+
)
593+
audit_row = audit_rows.first()
594+
assert audit_row.acting_user is None
595+
assert audit_row.data_after["id"] == enrollment.id
596+
assert audit_row.data_after["active"] is True
597+
598+
529599
@pytest.mark.parametrize("active", [True, False])
530600
@pytest.mark.parametrize("change_status", [None, *ALL_ENROLL_CHANGE_STATUSES])
531601
def test_create_program_enrollments_reactivation(user, active, change_status):
@@ -561,7 +631,11 @@ def test_create_program_enrollments_creation_fail(mocker, user):
561631
creation of local enrollment records
562632
"""
563633
programs = ProgramFactory.create_batch(2)
564-
enrollment = ProgramEnrollmentFactory.build(program=programs[1])
634+
# Use .create() (not .build()) so the enrollment has a primary key.
635+
# `create_program_enrollments` now calls `enrollment.save_and_log(None)`
636+
# when get_or_create reports created=True, which writes an audit row
637+
# via a ForeignKey to the enrollment and therefore requires a saved PK.
638+
enrollment = ProgramEnrollmentFactory.create(user=user, program=programs[1])
565639
mocker.patch(
566640
"courses.api.ProgramEnrollment.all_objects.get_or_create",
567641
side_effect=[Exception(), (enrollment, True)],

courses/management/commands/create_local_enrollments.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def handle(self, *args, **options): # noqa: ARG002
5454
user = User.objects.filter(username=edx_enrollment.user).first()
5555
if user:
5656
(
57-
_,
57+
enrollment,
5858
created,
5959
) = CourseRunEnrollment.all_objects.get_or_create(
6060
user=user,
@@ -68,6 +68,7 @@ def handle(self, *args, **options): # noqa: ARG002
6868
),
6969
)
7070
if created:
71+
enrollment.save_and_log(None)
7172
created_count[courseware_id] += 1
7273

7374
except: # noqa: PERF203, E722

0 commit comments

Comments
 (0)