Hi all,
Does anybody know where does data from the unenrollment form go? I don’t see any POST issued after submitting.
IvoBranco
(Ivo Branco)
July 29, 2025, 9:24pm
2
Hi @Andres.Aulasneo ,
I think it goes to <LMS_HOST>/admin/user_api/userretirementrequest/
.
Then the users are retired after 30 days if you have this Tutor plugin installed: GitHub - hastexo/tutor-contrib-retirement: This is an experimental Tutor plugin. You should not consider it ready for production use at this point.
Kind regards, Ivo Branco.
NAU Platform - FCT|FCCN
Thanks @IvoBranco ,
But the plugin you mention is for user retirement (delete user account completely from the site). The form I mentioned is shown when you unenroll from a course.
IvoBranco
(Ivo Branco)
July 30, 2025, 11:25am
4
Sorry, my bad!
I tested it on our environment that runs a based Redwood release.
It POST to <LMS_DOMAIN>/change_enrollment
with payload of:
course_id
: `course-v1:…. `
enrollment_action
: `unenroll`
From my analysis it’s saved to the CourseEnrollment class. You can view your records on `<LMS_DOMAIN>/admin/student/courseenrollment/`.
The unenrollments have the “Is Active” column as False
.
Hope it helps.
Calls and references on the code:
),
]
urlpatterns = [
path('', branding_views.index, name='root'), # Main marketing page, or redirect to courseware
path('', include('common.djangoapps.student.urls')),
# TODO: Move lms specific student views out of common code
re_path(r'^dashboard/?$', student_views.student_dashboard, name='dashboard'),
path('change_enrollment', student_views.change_enrollment, name='change_enrollment'),
# Event tracking endpoints
path('', include('common.djangoapps.track.urls')),
# Static template view endpoints like blog, faq, etc.
path('', include('lms.djangoapps.static_template_view.urls')),
path('heartbeat', include('openedx.core.djangoapps.heartbeat.urls')),
path('i18n/', include('django.conf.urls.i18n')),
enrollment = CourseEnrollment.get_enrollment(user, course_id)
if not enrollment:
return HttpResponseBadRequest(_("You are not enrolled in this course"))
certificate_info = cert_info(user, enrollment)
if certificate_info.get('status') in DISABLE_UNENROLL_CERT_STATES:
return HttpResponseBadRequest(_("Your certificate prevents you from unenrolling from this course"))
try:
CourseEnrollment.unenroll(user, course_id)
except UnenrollmentNotAllowed as exc:
return HttpResponseBadRequest(str(exc))
log.info("User %s unenrolled from %s; sending REFUND_ORDER", user.username, course_id)
REFUND_ORDER.send(sender=None, course_enrollment=enrollment)
return HttpResponse()
else:
return HttpResponseBadRequest(_("Enrollment action is invalid"))
try:
record = cls.objects.get(user=user, course_id=course_id)
try:
# .. filter_implemented_name: CourseUnenrollmentStarted
# .. filter_type: org.openedx.learning.course.unenrollment.started.v1
record = CourseUnenrollmentStarted.run_filter(enrollment=record)
except CourseUnenrollmentStarted.PreventUnenrollment as exc:
raise UnenrollmentNotAllowed(str(exc)) from exc
record.update_enrollment(is_active=False, skip_refund=skip_refund)
except cls.DoesNotExist:
log.error(
"Tried to unenroll student %s from %s but they were not enrolled",
user,
course_id
)
@classmethod
def unenroll_by_email(cls, email, course_id):
@classmethod
def is_enrollment_closed(cls, user, course):
"""
Returns a boolean value regarding whether the user has access to enroll in the course. Returns False if the
enrollment has been closed.
"""
from openedx.core.djangoapps.enrollments.permissions import ENROLL_IN_COURSE
return not user.has_perm(ENROLL_IN_COURSE, course)
def update_enrollment(self, mode=None, is_active=None, skip_refund=False, enterprise_uuid=None):
"""
Updates an enrollment for a user in a class. This includes options
like changing the mode, toggling is_active True/False, etc.
Also emits relevant events for analytics purposes.
This saves immediately.
"""
RequestCache('get_enrollment').clear()
Kind regards, Ivo Branco.