Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
597cd30
feat: add instructor task for async batch enrollment
BryanttV Aug 1, 2025
408e6c6
feat: add tests for submit_student_enrollment_batch api function
BryanttV Nov 26, 2025
847c02a
feat: add tests for async enrollment and unenrollment processes
BryanttV Nov 26, 2025
c62669d
feat: add unit tests for student enrollment utility functions
BryanttV Nov 26, 2025
783e8fa
refactor: update user identification method
BryanttV Nov 26, 2025
e1f5f2d
test: uncomment tests
BryanttV Nov 26, 2025
fa4d1c2
fix: satisfy ruff and pylint rules
BryanttV Apr 22, 2026
a11a0bc
fix: correct state transition
BryanttV Apr 22, 2026
6ba612f
fix: add type hints for request_user parameter and improve error hand…
BryanttV Apr 22, 2026
e44abe5
fix: improve error messages and task key generation in enrollment pro…
BryanttV Apr 22, 2026
59ab047
chore: implement suggestions from code review
BryanttV Apr 22, 2026
1d8343c
fix: update task key generation in tests
BryanttV Apr 22, 2026
ab9f19e
fix: update state transition in bulk enrollment tests
BryanttV May 7, 2026
d3f21e9
fix: disable pylint warning for imported User model in enrollment.py
BryanttV May 7, 2026
3fff477
fix: correct test method names and update state transitions in bulk e…
BryanttV May 8, 2026
fcb0700
fix: wrap enrollment sync process in a transaction to ensure atomicity
BryanttV May 8, 2026
8851b1e
feat: add site_id parameter to enrollment batch processing
BryanttV May 8, 2026
ea5a625
fix: add site_id to the enrollment batch submission test case
BryanttV May 8, 2026
479ff5c
fix: wrap enrollment and audit operations in a transaction for atomicity
BryanttV May 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 34 additions & 11 deletions lms/djangoapps/bulk_enroll/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
from opaque_keys.edx.keys import CourseKey
from rest_framework.test import APIRequestFactory, APITestCase, force_authenticate

from common.djangoapps.student.models import ( # pylint: disable=line-too-long
from common.djangoapps.student.models import (
ENROLLED_TO_ENROLLED,
ENROLLED_TO_UNENROLLED,
UNENROLLED_TO_ENROLLED,
CourseEnrollment,
Expand Down Expand Up @@ -150,6 +151,9 @@ def test_invalid_email(self):
{
"identifier": 'percivaloctavius@',
"invalidIdentifier": True,
"success": False,
"error_type": "invalid_identifier",
"error_message": "Invalid email address",
}
]
}
Expand Down Expand Up @@ -182,6 +186,9 @@ def test_invalid_username(self):
{
"identifier": 'percivaloctavius',
"invalidIdentifier": True,
"success": False,
"error_type": "invalid_identifier",
"error_message": "Invalid email address",
}
]
}
Expand Down Expand Up @@ -224,7 +231,9 @@ def test_enroll_with_username(self):
"auto_enroll": False,
"user": True,
"allowed": False,
}
},
"success": True,
"state_transition": UNENROLLED_TO_ENROLLED,
}
]
}
Expand Down Expand Up @@ -274,7 +283,9 @@ def test_enroll_with_email(self, use_json):
"auto_enroll": False,
"user": True,
"allowed": False,
}
},
"success": True,
"state_transition": UNENROLLED_TO_ENROLLED,
}
]
}
Expand Down Expand Up @@ -328,7 +339,9 @@ def test_unenroll(self, use_json):
"auto_enroll": False,
"user": True,
"allowed": False,
}
},
"success": True,
"state_transition": ENROLLED_TO_UNENROLLED,
}
]
}
Expand Down Expand Up @@ -432,7 +445,9 @@ def test_add_to_valid_cohort(self):
"user": True,
"allowed": False,
"cohort": 'cohort1',
}
},
"success": True,
"state_transition": UNENROLLED_TO_ENROLLED,
}
]
}
Expand All @@ -446,7 +461,7 @@ def test_add_to_valid_cohort(self):

assert res_json == expected

def test_readd_to_different_cohort(self):
def test_read_to_different_cohort(self):
config_course_cohorts(self.course, is_cohorted=True, manual_cohorts=["cohort1", "cohort2"])
response = self.request_bulk_enroll({
'identifiers': self.notenrolled_student.username,
Expand Down Expand Up @@ -483,7 +498,9 @@ def test_readd_to_different_cohort(self):
"user": True,
"allowed": False,
"cohort": 'cohort1',
}
},
"success": True,
"state_transition": UNENROLLED_TO_ENROLLED,
}
]
}
Expand Down Expand Up @@ -531,7 +548,9 @@ def test_readd_to_different_cohort(self):
"user": True,
"allowed": False,
"cohort": 'cohort2',
}
},
"success": True,
"state_transition": ENROLLED_TO_ENROLLED,
}
]
}
Expand All @@ -541,7 +560,7 @@ def test_readd_to_different_cohort(self):
assert get_cohort_id(self.notenrolled_student, CourseKey.from_string(self.course_key)) is not None
assert res2_json == expected2

def test_readd_to_same_cohort(self):
def test_read_to_same_cohort(self):
config_course_cohorts(self.course, is_cohorted=True, manual_cohorts=["cohort1", "cohort2"])
response = self.request_bulk_enroll({
'identifiers': self.notenrolled_student.username,
Expand Down Expand Up @@ -578,7 +597,9 @@ def test_readd_to_same_cohort(self):
"user": True,
"allowed": False,
"cohort": 'cohort1',
}
},
"success": True,
"state_transition": UNENROLLED_TO_ENROLLED,
}
]
}
Expand Down Expand Up @@ -627,7 +648,9 @@ def test_readd_to_same_cohort(self):
"user": True,
"allowed": False,
"cohort": 'cohort1',
}
},
"success": True,
"state_transition": ENROLLED_TO_ENROLLED,
}
]
}
Expand Down
2 changes: 1 addition & 1 deletion lms/djangoapps/bulk_enroll/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ def post(self, request): # pylint: disable=missing-function-docstring
# Internal request to DRF view
view = StudentsUpdateEnrollmentView()
response_content = view._process_student_enrollment( # pylint: disable=protected-access
user=request.user,
request=request,
course_id=course_id,
data=request.data,
secure=request.is_secure()
Expand Down
36 changes: 35 additions & 1 deletion lms/djangoapps/instructor/enrollment.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
from datetime import datetime

import pytz
from crum import get_current_request
from django.conf import settings
from django.contrib.auth.models import User # pylint: disable=imported-auth-user
from django.contrib.sites.models import Site
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils.translation import override as override_language
Expand Down Expand Up @@ -50,7 +52,9 @@
)
from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.theming.helpers import get_current_site
from openedx.core.djangoapps.user_api.models import UserPreference
from openedx.core.lib.celery.task_utils import emulate_http_request
from xmodule.modulestore.django import modulestore # pylint: disable=wrong-import-order
from xmodule.modulestore.exceptions import ItemNotFoundError # pylint: disable=wrong-import-order

Expand Down Expand Up @@ -591,7 +595,37 @@ def send_mail_to_student(student, param_dict, language=None):
language=language,
user_context=param_dict,
)
ace.send(message)

current_request = get_current_request()

if current_request is None:
# We're in a Celery task context, need to emulate HTTP request
site = get_current_site()
if not site:
try:
site = Site.objects.get(id=settings.SITE_ID)
except Site.DoesNotExist:
try:
site = Site.objects.first()
except Exception: # pylint: disable=broad-except
site = None

# Get the recipient user for tracking purposes
user = None
if lms_user_id and lms_user_id > 0:
try:
user = User.objects.get(id=lms_user_id)
except User.DoesNotExist:
pass

# Use emulate_http_request to provide the necessary context for template tags
# that require a request object, such as google_analytics_tracking_pixel
with emulate_http_request(site=site, user=user):
ace.send(message)
else:
# We're in a web context, just send the message directly
# The current request already provides the necessary context
ace.send(message)


def render_message_to_string(subject_template, message_template, param_dict, language=None):
Expand Down
12 changes: 12 additions & 0 deletions lms/djangoapps/instructor/message_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,15 @@ class RemoveBetaTester(BaseMessageType):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.options['transactional'] = True


class BatchEnrollment(BaseMessageType):
"""
A message for instructors when they finish the batch enrollment async process.
"""

APP_LABEL = "instructor"

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.options["transactional"] = True
Loading
Loading