From 033934e896d0f8b2a3d9d954901ef9062af4c2fa Mon Sep 17 00:00:00 2001 From: Maria Grimaldi Date: Fri, 28 Jan 2022 17:38:18 -0400 Subject: [PATCH] docs: add Open edX Events & Filters latest documentation --- docs/guides/extension_points.rst | 4 + docs/guides/hooks/events.rst | 179 +++++++++++++++++++++++++++++ docs/guides/hooks/filters.rst | 183 ++++++++++++++++++++++++++++++ docs/guides/hooks/index.rst | 186 ++----------------------------- 4 files changed, 375 insertions(+), 177 deletions(-) create mode 100644 docs/guides/hooks/events.rst create mode 100644 docs/guides/hooks/filters.rst diff --git a/docs/guides/extension_points.rst b/docs/guides/extension_points.rst index fe2d9315a4..8c2d9c86bd 100644 --- a/docs/guides/extension_points.rst +++ b/docs/guides/extension_points.rst @@ -140,6 +140,9 @@ Here are the different integration points that python plugins can use: * - Open edX Events - Adopt, Stable - Events are part of the greater Hooks Extension Framework for open extension of edx-platform. Events are a stable way for plugin developers to react to learner or author events. They are defined by a `separate events library`_ that developers can include in their requirements to develop and test the code without creating a dependency on this large repo. For more information see the `hooks guide`_. + * - Open edX Filters + - Adopt, Stable + - Filters are also part of Hooks Extension Framework for open extension of edx-platform. Filters are a flexible way for plugin developers to modify learner or author application flows. They are defined by a `separate filters library`_ that developers can include in their requirements to develop and test the code without creating a dependency on this large repo. For more information see the `hooks guide`_. .. _Application: https://docs.djangoproject.com/en/3.0/ref/applications/ .. _Django app plugin documentation: https://github.com/edx/edx-platform/blob/master/openedx/core/djangoapps/plugins/README.rst @@ -155,6 +158,7 @@ Here are the different integration points that python plugins can use: .. |pluggable_override docstring| replace:: ``pluggable_override`` docstring .. _pluggable_override docstring: https://github.com/edx/edx-django-utils/blob/master/edx_django_utils/plugins/pluggable_override.py .. _separate events library: https://github.com/eduNEXT/openedx-events/ +.. _separate filters library: https://github.com/eduNEXT/openedx-filters/ .. _hooks guide: https://github.com/edx/edx-platform/blob/master/docs/guides/hooks/index.rst Platform Look & Feel diff --git a/docs/guides/hooks/events.rst b/docs/guides/hooks/events.rst new file mode 100644 index 0000000000..4c0725d175 --- /dev/null +++ b/docs/guides/hooks/events.rst @@ -0,0 +1,179 @@ +Open edX Events +=============== + +How to use +---------- + +Using openedx-events in your code is very straight forward. We can consider the +two possible cases, sending or receiving an event. + + +Receiving events +^^^^^^^^^^^^^^^^ + +This is one of the most common use cases for plugins. The edx-platform will send +and event and you want to react to it in your plugin. + +For this you need to: + +1. Include openedx-events in your dependencies. +2. Connect your receiver functions to the signals being sent. + +Connecting signals can be done using regular django syntax: + +.. code-block:: python + + from openedx_events.learning.signals import SESSION_LOGIN_COMPLETED + + @receiver(SESSION_LOGIN_COMPLETED) + # your receiver function here + + +Or at the apps.py + +.. code-block:: python + + "signals_config": { + "lms.djangoapp": { + "relative_path": "your_module_name", + "receivers": [ + { + "receiver_func_name": "your_receiver_function", + "signal_path": "openedx_events.learning.signals.SESSION_LOGIN_COMPLETED", + }, + ], + } + }, + +In case you are listening to an event in the edx-platform repo, you can directly +use the django syntax since the apps.py method will not be available without the +plugin. + + +Sending events +^^^^^^^^^^^^^^ + +Sending events requires you to import both the event definition as well as the +attr data classes that encapsulate the event data. + +.. code-block:: python + + from openedx_events.learning.data import UserData, UserPersonalData + from openedx_events.learning.signals import STUDENT_REGISTRATION_COMPLETED + + STUDENT_REGISTRATION_COMPLETED.send_event( + user=UserData( + pii=UserPersonalData( + username=user.username, + email=user.email, + name=user.profile.name, + ), + id=user.id, + is_active=user.is_active, + ), + ) + +You can do this both from the edx-platform code as well as from an openedx +plugin. + + +Testing events +^^^^^^^^^^^^^^ + +Testing your code in CI, specially for plugins is now possible without having to +import the complete edx-platform as a dependency. + +To test your functions you need to include the openedx-events library in your +testing dependencies and make the signal connection in your test case. + +.. code-block:: python + + from openedx_events.learning.signals import STUDENT_REGISTRATION_COMPLETED + + def test_your_receiver(self): + STUDENT_REGISTRATION_COMPLETED.connect(your_function) + STUDENT_REGISTRATION_COMPLETED.send_event( + user=UserData( + pii=UserPersonalData( + username='test_username', + email='test_email@example.com', + name='test_name', + ), + id=1, + is_active=True, + ), + ) + + # run your assertions + + +Changes in the openedx-events library that are not compatible with your code +should break this kind of test in CI and let you know you need to upgrade your +code. + + +Live example +^^^^^^^^^^^^ + +For a complete and detailed example you can see the `openedx-events-2-zapier`_ +plugin. This is a fully functional plugin that connects to +``STUDENT_REGISTRATION_COMPLETED`` and ``COURSE_ENROLLMENT_CREATED`` and sends +the relevant information to zapier.com using a webhook. + +.. _openedx-events-2-zapier: https://github.com/eduNEXT/openedx-events-2-zapier + + +Index of Events +----------------- + +This list contains the events currently being sent by edx-platform. The provided +links target both the definition of the event in the openedx-events library as +well as the trigger location in this same repository. + + +.. list-table:: + :widths: 35 50 20 + + * - *Name* + - *Type* + - *Date added* + + * - `STUDENT_REGISTRATION_COMPLETED `_ + - org.openedx.learning.student.registration.completed.v1 + - `2022-06-14 `_ + + * - `SESSION_LOGIN_COMPLETED `_ + - org.openedx.learning.auth.session.login.completed.v1 + - `2022-06-14 `_ + + * - `COURSE_ENROLLMENT_CREATED `_ + - org.openedx.learning.course.enrollment.created.v1 + - `2022-06-14 `_ + + * - `COURSE_ENROLLMENT_CHANGED `_ + - org.openedx.learning.course.enrollment.changed.v1 + - `2022-06-14 `_ + + * - `COURSE_UNENROLLMENT_COMPLETED `_ + - org.openedx.learning.course.unenrollment.completed.v1 + - `2022-06-14 `_ + + * - `CERTIFICATE_CREATED `_ + - org.openedx.learning.certificate.created.v1 + - `2022-06-14 `_ + + * - `CERTIFICATE_CHANGED `_ + - org.openedx.learning.certificate.changed.v1 + - `2022-06-14 `_ + + * - `CERTIFICATE_REVOKED `_ + - org.openedx.learning.certificate.revoked.v1 + - `2022-06-14 `_ + + * - `COHORT_MEMBERSHIP_CHANGED `_ + - org.openedx.learning.cohort_membership.changed.v1 + - `2022-06-14 `_ + + * - `COURSE_DISCUSSIONS_CHANGED `_ + - org.openedx.learning.discussions.configuration.changed.v1 + - `2022-06-14 `_ diff --git a/docs/guides/hooks/filters.rst b/docs/guides/hooks/filters.rst new file mode 100644 index 0000000000..d2f15c7997 --- /dev/null +++ b/docs/guides/hooks/filters.rst @@ -0,0 +1,183 @@ +Open edX Filters +================ + +How to use +---------- + +Using openedx-filters in your code is very straight forward. We can consider the +two possible cases: + +Configuring a filter +^^^^^^^^^^^^^^^^^^^^ + +Implement pipeline steps +************************ + +Let's say you want to consult student's information with a third party service +before generating the students certificate. This is a common use case for filters, +where the functions part of the filter's pipeline will perform the consulting tasks and +decide the execution flow for the application. These functions are the pipeline steps, +and can be implemented in an installable Python library: + +.. code-block:: python + + # Step implementation taken from openedx-filters-samples plugin + from openedx_filters import PipelineStep + from openedx_filters.learning.filters import CertificateCreationRequested + + class StopCertificateCreation(PipelineStep): + + def run_filter(self, user, course_id, mode, status): + # Consult third party service and check if continue + # ... + # User not in third party service, denied certificate generation + raise CertificateCreationRequested.PreventCertificateCreation( + "You can't generate a certificate from this site." + ) + +There's two key components to the implementation: + +1. The filter step must be a subclass of ``PipelineStep``. + +2. The ``run_filter`` signature must match the filters definition, eg., +the previous step matches the method's definition in CertificateCreationRequested. + +Attach/hook pipeline to filter +****************************** + +After implementing the pipeline steps, we have to tell the certificate creation +filter to execute our pipeline. + +.. code-block:: python + + OPEN_EDX_FILTERS_CONFIG = { + "org.openedx.learning.certificate.creation.requested.v1": { + "fail_silently": False, + "pipeline": [ + "openedx_filters_samples.samples.pipeline.StopCertificateCreation" + ] + }, + } + +Triggering a filter +^^^^^^^^^^^^^^^^^^^ + +In order to execute a filter in your own plugin/library, you must install the +plugin where the steps are implemented and also, ``openedx-filters``. + +.. code-block:: python + + # Code taken from lms/djangoapps/certificates/generation_handler.py + from openedx_filters.learning.filters import CertificateCreationRequested + + try: + self.user, self.course_id, self.mode, self.status = CertificateCreationRequested.run_filter( + user=self.user, course_id=self.course_id, mode=self.mode, status=self.status, + ) + except CertificateCreationRequested.PreventCertificateCreation as exc: + raise CertificateGenerationNotAllowed(str(exc)) from exc + +Testing filters' steps +^^^^^^^^^^^^^^^^^^^^^^ + +It's pretty straightforward to test your pipeline steps, you'll need to include the +``openedx-filters`` library in your testing dependencies and configure them in your test case. + +.. code-block:: python + + from openedx_filters.learning.filters import CertificateCreationRequested + + @override_settings( + OPEN_EDX_FILTERS_CONFIG={ + "org.openedx.learning.certificate.creation.requested.v1": { + "fail_silently": False, + "pipeline": [ + "openedx_filters_samples.samples.pipeline.StopCertificateCreation" + ] + } + } + ) + def test_certificate_creation_requested_filter(self): + """ + Test filter triggered before the certificate creation process starts. + + Expected results: + - The pipeline step configured for the filter raises PreventCertificateCreation + when the conditions are met. + """ + with self.assertRaises(CertificateCreationRequested.PreventCertificateCreation): + CertificateCreationRequested.run_filter( + user=self.user, course_key=self.course_key, mode="audit", + ) + + # run your assertions + +Changes in the ``openedx-filters`` library that are not compatible with your code +should break this kind of test in CI and let you know you need to upgrade your code. +The main limitation while testing filters' steps it's their arguments, as they are edxapp +memory objects, but that can be solved in CI using Python mocks. + +Live example +^^^^^^^^^^^^ + +For filter steps samples you can visit the `openedx-filters-samples`_ plugin, where +you can find minimal steps exemplifying the different ways on how to use +``openedx-filters``. + +.. _openedx-filters-samples: https://github.com/eduNEXT/openedx-filters-samples + + +Index of Filters +----------------- + +This list contains the filters currently being executed by edx-platform. The provided +links target both the definition of the filter in the openedx-filters library as +well as the trigger location in this same repository. + + +.. list-table:: + :widths: 35 50 20 + + * - *Name* + - *Type* + - *Date added* + + * - `StudentRegistrationRequested `_ + - org.openedx.learning.student.registration.requested.v1 + - `2022-06-14 `_ + + * - `StudentLoginRequested `_ + - org.openedx.learning.student.login.requested.v1 + - `2022-06-14 `_ + + * - `CourseEnrollmentStarted `_ + - org.openedx.learning.course.enrollment.started.v1 + - `2022-06-14 `_ + + * - `CourseUnenrollmentStarted `_ + - org.openedx.learning.course.unenrollment.started.v1 + - `2022-06-14 https://github.com/eduNEXT/edx-platform/blob/master/common/djangoapps/student/models.py#L1752>`_ + + * - `CertificateCreationRequested `_ + - org.openedx.learning.certificate.creation.requested.v1 + - `2022-06-14 `_ + + * - `CertificateRenderStarted `_ + - org.openedx.learning.certificate.render.started.v1 + - `2022-06-14 `_ + + * - `CohortChangeRequested `_ + - org.openedx.learning.cohort.change.requested.v1 + - `2022-06-14 `_ + + * - `CohortAssignmentRequested `_ + - org.openedx.learning.cohort.assignment.requested.v1 + - `2022-06-14 `_ + + * - `CourseAboutRenderStarted `_ + - org.openedx.learning.course_about.render.started.v1 + - `2022-06-14 `_ + + * - `DashboardRenderStarted `_ + - org.openedx.learning.dashboard.render.started.v1 + - `2022-06-14 `_ diff --git a/docs/guides/hooks/index.rst b/docs/guides/hooks/index.rst index cffc878526..e891adb974 100644 --- a/docs/guides/hooks/index.rst +++ b/docs/guides/hooks/index.rst @@ -22,8 +22,8 @@ before this data is put back in the original application flow. In order to allow extension developers to use the Events and Filters definitions on their plugins, both kinds of hooks are defined in lightweight external libraries. -* `openedx filters`_ -* `openedx events`_ +* `openedx-filters`_ +* `openedx-events`_ Hooks are designed with stability in mind. The main goal is that developers can use them to change the functionality of the platform as needed and still be able @@ -36,8 +36,8 @@ A longer description of the framework and it's history can be found in `OEP 50`_ .. _OEP 50: https://open-edx-proposals.readthedocs.io/en/latest/oep-0050-hooks-extension-framework.html .. _versioning ADR: https://github.com/eduNEXT/openedx-events/blob/main/docs/decisions/0002-events-naming-and-versioning.rst .. _payload ADR: https://github.com/eduNEXT/openedx-events/blob/main/docs/decisions/0003-events-payload.rst -.. _openedx filters: https://github.com/eduNEXT/openedx-filters -.. _openedx events: https://github.com/eduNEXT/openedx-events +.. _openedx-filters: https://github.com/eduNEXT/openedx-filters +.. _openedx-events: https://github.com/eduNEXT/openedx-events On the technical side events are implemented through django signals which makes them run in the same python process as the lms or cms. Furthermore, events block @@ -45,176 +45,8 @@ the running process. Listeners of an event are encouraged to monitor the performance or use alternative arch patterns such as receiving the event and defer to launching async tasks than do the slow processing. - -How to use ----------- - -Using openedx-events in your code is very straight forward. We can consider the -two possible cases, sending or receiving an event. - - -Receiving events -^^^^^^^^^^^^^^^^ - -This is one of the most common use cases for plugins. The edx-platform will send -and event and you want to react to it in your plugin. - -For this you need to: - -1. Include openedx-events in your dependencies. -2. Connect your receiver functions to the signals being sent. - -Connecting signals can be done using regular django syntax: - -.. code-block:: python - - from openedx_events.learning.signals import SESSION_LOGIN_COMPLETED - - @receiver(SESSION_LOGIN_COMPLETED) - # your receiver function here - - -Or at the apps.py - -.. code-block:: python - - "signals_config": { - "lms.djangoapp": { - "relative_path": "your_module_name", - "receivers": [ - { - "receiver_func_name": "your_receiver_function", - "signal_path": "openedx_events.learning.signals.SESSION_LOGIN_COMPLETED", - }, - ], - } - }, - -In case you are listening to an event in the edx-platform repo, you can directly -use the django syntax since the apps.py method will not be available without the -plugin. - - -Sending events -^^^^^^^^^^^^^^ - -Sending events requires you to import both the event definition as well as the -attr data classes that encapsulate the event data. - -.. code-block:: python - - from openedx_events.learning.data import UserData, UserPersonalData - from openedx_events.learning.signals import STUDENT_REGISTRATION_COMPLETED - - STUDENT_REGISTRATION_COMPLETED.send_event( - user=UserData( - pii=UserPersonalData( - username=user.username, - email=user.email, - name=user.profile.name, - ), - id=user.id, - is_active=user.is_active, - ), - ) - -You can do this both from the edx-platform code as well as from an openedx -plugin. - - -Testing events -^^^^^^^^^^^^^^ - -Testing your code in CI, specially for plugins is now possible without having to -import the complete edx-platform as a dependency. - -To test your functions you need to include the openedx-events library in your -testing dependencies and make the signal connection in your test case. - -.. code-block:: python - - from openedx_events.learning.signals import STUDENT_REGISTRATION_COMPLETED - - def test_your_receiver(self): - STUDENT_REGISTRATION_COMPLETED.connect(your_function) - STUDENT_REGISTRATION_COMPLETED.send_event( - user=UserData( - pii=UserPersonalData( - username='test_username', - email='test_email@example.com', - name='test_name', - ), - id=1, - is_active=True, - ), - ) - - # run your assertions - - -Changes in the openedx-events library that are not compatible with your code -should break this kind of test in CI and let you know you need to upgrade your -code. - - -Live example -^^^^^^^^^^^^ - -For a complete and detailed example you can see the `openedx-events-2-zapier`_ -plugin. This is a fully functional plugin that connects to -``STUDENT_REGISTRATION_COMPLETED`` and ``COURSE_ENROLLMENT_CREATED`` and sends -the relevant information to zapier.com using a webhook. - -.. _openedx-events-2-zapier: https://github.com/eduNEXT/openedx-events-2-zapier - - -Index of Events ------------------ - -This list contains the events currently being sent by edx-platform. The provided -links target both the definition of the event in the openedx-events library as -well as the trigger location in this same repository. - - -.. list-table:: - :widths: 35 50 20 - - * - *Name* - - *Type* - - *Date added* - - * - `STUDENT_REGISTRATION_COMPLETED `_ - - org.openedx.learning.student.registration.completed.v1 - - `2021-09-02 `__ - - * - `SESSION_LOGIN_COMPLETED `_ - - org.openedx.learning.auth.session.login.completed.v1 - - `2021-09-02 `__ - - * - `COURSE_ENROLLMENT_CREATED `_ - - org.openedx.learning.course.enrollment.created.v1 - - `2021-09-02 `__ - - * - `COURSE_ENROLLMENT_CHANGED `_ - - org.openedx.learning.course.enrollment.changed.v1 - - `2021-09-22 `__ - - * - `COURSE_UNENROLLMENT_COMPLETED `_ - - org.openedx.learning.course.unenrollment.completed.v1 - - `2021-09-22 `__ - - * - `CERTIFICATE_CREATED `_ - - org.openedx.learning.certificate.created.v1 - - `2021-09-22 `__ - - * - `CERTIFICATE_CHANGED `_ - - org.openedx.learning.certificate.changed.v1 - - `2021-09-22 `__ - - * - `CERTIFICATE_REVOKED `_ - - org.openedx.learning.certificate.revoked.v1 - - `2021-09-22 `__ - - * - `COHORT_MEMBERSHIP_CHANGED `_ - - org.openedx.learning.cohort_membership.changed.v1 - - `2021-09-22 `__ +On the other hand, filters are implemented using a pipeline mechanism, that executes +a list of functions called ``steps`` configured through Django settings. Each +pipeline step receives a dictionary with data, process it and returns an output. During +this process, they can alter the application execution flow by halting the process +or modifying their input arguments.