Suppose that you need something special to happen in Open edX every time a user is authenticated? for example, suppose that you want to run an A/B split test, or that you need to send an email message to a small select group of active users like say, only the users who are currently enrolled in the paid versions of your courses. There are a few ways to accomplish this, but one method in particular really stands out:
Django Dispatch Signals is preferable for a couple of reasons. First, Dispatch Signals enable you to avoid, or at least minimize, modifications to existing source code in the edx-platform repository. This not only greatly reduces the probability of introducing buggy behavior, but it also simplifies future upgrades of the software. Additionally, it is also a more fool-proof approach if your intention is to add custom functionality to ALL user authentications regardless of how users authenticated.
Importantly, Django dispatches a signal each time a user is authenticated, regardless of the authentication method. Using Django Signal Dispatch you can listen for and add your custom code to the login sequence of all user authentications even if your Open edX platform offers users multiple ways to authenticate (username & password, LTI, oAuth, etc.) Your code will even execute if you bypass Open edX LMS/CMS applications entirely and login directly to the Django admin console.
In this tutorial we will scaffold a small custom Django app in a fork of the edx-platform repository. You can reference the exact source code here for the complete Django app.
A Practical Example
The following code is the gist of how to get Django Dispatch Signals working in your Open edX project. Keep in mind that this is a simplification of the actual code that you would use for your project, so definitely use the real source code in the github repository rather than copying & pasting these snippets.
Only two new Python modules are required, as presented below.
1.) Register your new Django app
The first four lines of the module that follows will register a new Django app named common.djangoapps.custom_login. This is all that is required to create a completely new app within Open edX. Just imagine: limitless possibilities!!!The bottom two lines are less intuitive: ready() is a method this is part of Django’s AppConfig class and which fires immediately after the app has been registered with the Django framework at project startup. We leverage the ready() method to import our custom Python module receivers.py only after everything else is Django has loaded, thus averting a hazardous race situation. Perhaps equally unintuitive is why we need to import receivers.py in the first place since it clearly is not used in our implementation of ready(). We import receivers.py here because this is how we activate the Django receiver decorators in the module. That is, the code line,
from django.apps import AppConfig class CustomLoginConfig(AppConfig): name = 'common.djangoapps.custom_login' verbose_name = 'Sample Custom Login Helpers' def ready(self): from .signals import receivers
2.) Create your custom Python functions
This next Python module is pretty straightforward, but I should point out that you should try to contain all of your Django Dispatch Receivers in a single Python module if at all possible. Below, our custom Python function, my_custom_login_stuff(), gets executed any time the Python decorator @receiver() receives a “user_logged_in” signal. Our one-line function doesn’t do anything interesting, but you can review the actual source code for receivers.py in the Github repository to see a more real-world example how this could be put to work in your Open edX installation.
from django.dispatch import receiver from django.contrib.auth.signals import user_logged_in @receiver(user_logged_in) def my_custom_login_stuff(sender, request, user, **kwargs): print('my_custom_login_stuff() was called.')
3.) Add your new Django app to INSTALLED_APPS
If you are unfamiliar with the concept of INSTALLED_APPS then you can read more about it in the official Django documentation on adding Django applications to a Django project. The salient points for purposes of this tutorial are that it is important to add your new Django app to the BOTTOM of common.py, and that you should use INSTALLED_APPS.append() rather than any other alternative which you might otherwise have preferred.
# add this to the BOTTOM of common.py INSTALLED_APPS.append('common.djangoapps.custom_login.apps.CustomLoginConfig')
4.) Restart your Open edX platform
Modifications to any of the Python configuration files located in /edx/app/edxapp/edx-platform/lms/envs are only read at project startup, which happens either when you reboot the server or use the command line
/edx/bin/supervisorctl restart lms to restart the LMS application.
There Are Many Custom Open edX Dispatch Signals
Even though this particular article is about customizing authentication behavior, it turns out that Open edX extends Django’s built-in Dispatch functionality with many other useful Dispatch Signals that you can leverage for a variety of use cases. My code sample, in addition to enhancing the authentication behavior for example, also demonstrates how to use the custom ENROLLMENT_TRACK_UPDATED signal to add custom functionality when a user upgrades from an Audited to a Paid enrollment track using the e-commerce module.
Following are a few more of Open edX’s custom Dispatch Signals.
|User Management Signals||Description|
|USER_FIELD_CHANGED||Used to signal a field value change in the user (aka “student”) table|
|LEARNER_NOW_VERIFIED||Signal that indicates that a user has become verified for certificate purposes|
|USER_ACCOUNT_ACTIVATED||Signal indicating email verification was successfully completed|
|REGISTER_USER||Signal indicating that a new user was successfully created.|
|Enrollment and Course Progress||Description|
|ENROLLMENT_TRACK_UPDATED||Signal that indicates that a student has modified their enrollment mode. This is most commonly fired via e-commerce events when a student pays for a Verified Certificate for example.|
|SCORE_PUBLISHED||Signal that indicates that a student just submitted a response to a graded assignment problem including for example, homework, quiz, mid-term and final exam assignment types.|
|COURSE_CERT_AWARDED||Signal that indicates that a student was just awarded a course completion certificate|
|COURSE_GRADE_NOW_PASSED||Signal that indicates that a student just achieved the minimum passing grade for a course.|
|COURSE_GRADE_NOW_FAILED||Signal that indicates that a student’s grade status just changed from “passing” to “not passing”.|
|Email List Management||Description|
|USER_RETIRE_MAILINGS||Signal to retire a user from LMS-initiated mailings (course mailings, etc)|
|USER_RETIRE_LMS_CRITICAL||Signal to retire LMS critical information|
|USER_RETIRE_LMS_MISC||Signal to retire LMS misc information|
Why is your code added to “common”?
Common is a Django project that is fully contained within the edx-platform repository. It contains a set of Django apps that are shared between Course Management Studio (cms) and the Learning Management System (lms). Note that the edx-platform repository contains four distinct Django projects: cms, common, lms, and openedx. Strictly technically speaking, you could place your custom Django app in any of these four. So, why did we choose “common”?
At the risk of over-complicating matters, there is a fifth option which I actually prefer above these four obvious possibilities, which is to bundle your code as a standalone git repository that is added to requirements.txt and installed by pip. We ignore this possibility in this tutorial solely because it requires a deep understanding of pip as well as some subtleties of how the developers at edX go about managing the sizable number of dependencies in the Open edX project.
We chose “common” because it is in the application search paths of both lms and cms. Though unimportant for purposes of this tutorial, this could turn out to be helpful for other future custom programming tasks.
We avoided “openedx”, which for avoidance of any doubt, is also in the search paths of lms and cms, but which contains a more complicated file system structure that potentially adds a bit more complexity to how we might have to address and import our custom code.
The actual source code in Github includes a namespace. This is important.
In my forked repository of edx-platform the actual address to the custom Django app is
common.djangoapps.lawrencemcdaniel.custom_login.apps.CustomLoginConfig, which includes the name space “lawrencemcdaniel”. Consolidating all of your custom Django apps inside your own name space is an important strategy for avoiding future naming collisions in future releases of Open edX software.
Why use Django Dispatch rather than creating a custom user model, which is exactly what Django suggests in their documentation?
Yeah, great question! why not just do what Django tells you to do in their official documentation?
Django provides a way customize authentications, by creating a custom user model. So, why not do this? The short answer is that edX has already done this, by implementing the “student” Django app which is also their alternative Django user model. Thus, in Open edX software all users (learners, instructors, staff, admins) are “student” regardless of whether or not that’s true in real life.
The first problem with subclassing edX’s subclass of the Django user model is that your new code would touch literally every part of the Open edX code base, not only in lms but also cms. Thus you would need to exhaustively test every line of edx-platform just to determine if your custom subclass of “student” breaks anything in the current code base (which it very likely would). But then you would also have to re-test just as exhaustively for all future releases of the software, and you would run the same high risk of your custom user model breaking future code as well.
So, the longer answer is that this approach is bad risk-reward.
My learners authenticate via oAuth and third_party_auth provides a way to customize authentications via pipeline.py
If you’re already familiar with how pipeline.py works, why not put your customization there? The problem with pipeline.py is that it is only called in the cases where authentication occurred via some form of third party authentication. Your code will be skipped entirely if users authenticate with a username and password.
Implementing your customized login via Django Dispatch Signals ensures that it is applied to all users in all authentication scenarios.