How to create a plugin that listens to Open edX events?

Hi,

For the last 2 days I’ve been trying to create a simple plugin that listens to one of openEdx events, then print some logs (just for testing purpose).
However, I’ve been struggling to make it work.

My question is the following:
In order to be able to listen to openEdx events, should I create a new django app like this one openedx-plugin-example ? or I can do it by creating only one python file and enable it using tutuor plugins enable like in this doc Using-Events ?
and if the answer is creating a new django app like in the repo, what are the next steps to integrate that app with my openEdx instance that is already running on a server ?

I will appreciate any answers, suggestions or even links for extra documenation.

Thank you.

Hi @KenoIsPlaying ! For most cases you would want to use something like the openedx-plugin-example to include the code in the “Using Events” doc to be able to listen to the events.

If you are using Tutor, you can then make a small tutor plugin to install your package and get it built into the openedx image, which is separate process. While developing, it’s often easier to mount your package into Tutor, then rebuild the openedx image once. After that your code updates should show up automatically in tutor dev mode.

Hopefully that’s enough to get you started, but let us know if you run into any trouble.

2 Likes

@BrianMesick Thank you so much for the reply,
is there any code example that I can follow to consume/listen open-edx events?
from what I understood I should create a python file in tutor-plugins directory that uses django receivers above the function that will be triggered as a callback once the event happens.
so the plugin code will be something like this

from django.dispatch import receiver
from openedx_events.learning.signals import SESSION_LOGIN_COMPLETED
@receiver(SESSION_LOGIN_COMPLETED)
def send_test_email(user, **kwargs):
    # here I implement my logic

Then I should enable it using tutor plugins enable myPluginName
Can you please correct me if I’m wrong at some point.

Thank you.

Hi @KenoIsPlaying , there are two different plugins in play to do this:

  1. The code that runs inside the openedx container and listens for the events
  2. The code that tells Tutor to install the code from #1

What you are referring to is the Tutor plugin (#2), which can’t listen for openedx events as it’s not installed in the openedx runtime, but rather runs in the Tutor context.

For #1 you need something like the openedx-plugin-example to create a Django app that can be loaded in the openedx runtime at startup and connect your signal handlers so that you receive the events. Your event listener code goes here.

#2 handles installing that package as part of the tutor process. Either by using mounts as I suggested above (for development) or by injecting the dependency. If your code from #1 lives in github you can actually install it without a plugin as described here: Configuration and customisation — Tutor documentation but that’s a pain for iterating during development and mounts are easier.

It’s important to remember that for your code in #1 to work, it needs to be loaded at Django startup, and so needs to be installed and configured. If you look at the apps.py in the openedx-plugin-example it shows a very full featured version of this, but the important thing is that you have an apps.py that handles connecting your signal handlers on startup.

1 Like

Hi @BrianMesick Thank you again for your time, I do really appreciate it,

Now I have better understanding of the things that I should do in order to listen to an event thanks to you.

1- First I use the appPluginExample or I use cookieCutter to start an app
2- I modify the setup.py especially the entryPoints object so it will have lms.djangoapp as the key (i’m not sure about this point ).
example:

setup(
    name="tutor-contrib-demo",
    packages=find_packages(exclude=["tests*"]),
    include_package_data=True,
    python_requires=">=3.8",
    install_requires=["tutor>=17.0.0,<18.0.0"],
    entry_points={
        "lms.djangoapp": [
            "demo = tutordemo.apps:MyAppConfig",
        ],        
    },

)

2- I modify the apps.py to map between the events and the functions that will handle each event.
example:

class MyAppConfig(AppConfig):
    name = 'tutordemo'  
    verbose_name = "Tutor Demo Plugin"
    
    plugin_app = {
        'signals_config': {
            'lms.djangoapp': {
                'relative_path': 'plugin',  # this is the file that holds the logic of the functions
                'receivers': [{
                    'receiver_func_name': 'on_login_receiver',  
                    'signal_path': 'openedx_events.learning.signal.SESSION_LOGIN_COMPLETED',  
                }],
            }
        }
    }

3- I create a file with the logic of the functions mentioned in the apps.py
example

def on_login_receiver(user, **kwargs):
    print("It is finally working :D")
    print("=================================================")

So, If I have everything correct, I should mount the app into openEdx
after, I’ve read the link you sent me and I was doing it this way

tutor mounts add /home/ubuntu/tutor-contrib-demo

I rebuild the images using this command

tutor images build openedx

Unfortunately, it did not work, I think I’m pretty close to make it work.

If you have any extra notes I will be glad to take it.

Thank you.

2 Likes

Very interesting,
do you believe that we could listen to the polls and survey answered by learners in order to get some analytics and timestamps ?
Thanks @KenoIsPlaying @BrianMesick

@KenoIsPlaying It looks like you’re on the right track, but I can’t do much more without seeing some logs. You can reach out to me on the Open edX Slack if you want some help debugging.

@Amaury_Van_Espen it’s possible, but it would depend on which poll / survey tool you’re using (the community uses a few different technologies here), the Open edX version, and how you gather / display your analytics. It’s likely that the poll/survey code itself would need to be updated to log when a submission is received. There are also privacy considerations around doing this if the submissions are intended to be anonymous. If you’d like to open a separate topic for it we can kick around some options.

Got it @BrianMesick see Listen to openEdx Polls & Survey events

@BrianMesick I have managed to create my django app plugin, host it in a repository and I have successfully integrated it with openEdx using the OPENEDX_EXTRA_PIP_REQUIREMENTS attribute, now I’m sure that the app is working fine and is correct. However, as you mentioned before it is so hard to develop while rebuilding the whole platform with no cache in order to get the last changes in the repository, that’s why I was trying to use the mounts method,

I could not make it work yet, I’ve been mounting the app to this path /openedx/edx-platform/my app, to be more precise I’m using this command

tutor mounts add /home/ubuntu/openedx-plugin-example/openedx_plugin:/openedx/edx-platform/my-app

It seems like my app is not being loaded because I do not see any logs coming from the app, so if you have any advice I will appreciate it.

Note: when opening the LMS container bash I can see my app in the path where I mounted it, but still not working.

Thank you.

@KenoIsPlaying nice to hear you’re making progress! After you add a mount to tutor you need to rebuild the openedx image once to get the mount working:

For tutor dev it’s: tutor images build openedx-dev
For tutor local it’s: tutor images build openedx (but for local you need to do a tutor local reboot every time you want to see your changes, dev watches for changes and restarts automatically).

I’m working with events now too. My solution is to add two patches:

  • openedx-dockerfile-pre-assets (this copy my module to OpenedX container)
COPY --chown=app:app ./<my event backend module> ./<my event backend module>
  • openedx-lms-common-settings (here I’m add my event listener to settings)
EVENT_TRACKING_BACKENDS.update(
    {
        '<my backend>': {
            'ENGINE': 'eventtracking.backends.routing.RoutingBackend',
            'OPTIONS': {
                'backends': {
                    'scos': {
                        'ENGINE': '<my event backend module>.backend.MyBackendClass',
                    }
                },
                'processors': [
                    {
                        'ENGINE': 'eventtracking.processors.regex_filter.RegexFilter',
                        'OPTIONS':{
                            'filter_type': 'allowlist',
                            'regular_expressions': [
                                'edx.course.enrollment.activated',
                                'edx.course.enrollment.deactivated',
                            ]
                        }
                    }
                ]
            }
        }
    }
)

And in my plugin:

# folder in my plugin with files that uses on image build stage
MY_BUILD: str = pkg_resources.resource_filename("myplugin", "build")
ROOT_PATH: str = (os.path.expanduser(os.environ.get("TUTOR_ROOT", ""))
    or appdirs.user_data_dir(__app__)
)
# copy <my event backend module> to build folder where docker can pick it up
shutil.copytree(
    os.path.join(MY_BUILD, "<my event backend module>"),
    os.path.join(ROOT_PATH, "env/build/openedx/<my event backend module>"),
    dirs_exist_ok=True
)

In my backend.py, that in <my event backend module> :

class MyBackendClass:

    def send(self, event):

        print('\n!!!!!!!!!!\n', event, '\n!!!!!!!!!!\n')

When user enroll or unenroll from course it printing in log.

This links explain more:

And docs:
https://edx.readthedocs.io/projects/devdata/en/stable/internal_data_formats/tracking_logs.html