Merge pull request #27157 from eduNEXT/fmo/initial_hook_framework_docs

[BD-32] Hooks framework documentation
This commit is contained in:
Felipe Montoya
2021-10-14 10:39:29 -05:00
committed by GitHub
7 changed files with 234 additions and 2 deletions

View File

@@ -1439,6 +1439,7 @@ class CourseEnrollment(models.Model):
CourseEnrollmentState(self.mode, self.is_active),
)
# .. event_implemented_name: COURSE_ENROLLMENT_CHANGED
COURSE_ENROLLMENT_CHANGED.send_event(
enrollment=CourseEnrollmentData(
user=UserData(
@@ -1465,6 +1466,7 @@ class CourseEnrollment(models.Model):
self.emit_event(EVENT_NAME_ENROLLMENT_DEACTIVATED)
self.send_signal(EnrollStatusChange.unenroll)
# .. event_implemented_name: COURSE_UNENROLLMENT_COMPLETED
COURSE_UNENROLLMENT_COMPLETED.send_event(
enrollment=CourseEnrollmentData(
user=UserData(
@@ -1671,7 +1673,7 @@ class CourseEnrollment(models.Model):
enrollment.update_enrollment(is_active=True, mode=mode, enterprise_uuid=enterprise_uuid)
enrollment.send_signal(EnrollStatusChange.enroll)
# Announce user's enrollment
# .. event_implemented_name: COURSE_ENROLLMENT_CREATED
COURSE_ENROLLMENT_CREATED.send_event(
enrollment=CourseEnrollmentData(
user=UserData(

View File

@@ -137,6 +137,9 @@ Here are the different integration points that python plugins can use:
* - Pluggable override (``edx_django_utils.plugins.pluggable_override.pluggable_override``)
- Trial, Stable
- This decorator allows overriding any function or method by pointing to an alternative implementation in settings. Read the |pluggable_override docstring|_ to learn more.
* - 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`_.
.. _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
@@ -151,6 +154,8 @@ Here are the different integration points that python plugins can use:
.. _UserPartition docstring: https://github.com/edx/edx-platform/blob/f8cc58618a39c9f7b8e9e1001eb2d7a10395797e/common/lib/xmodule/xmodule/partitions/partitions.py#L105-L120
.. |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/
.. _hooks guide: https://github.com/edx/edx-platform/blob/master/docs/guides/hooks/index.rst
Platform Look & Feel
====================

220
docs/guides/hooks/index.rst Normal file
View File

@@ -0,0 +1,220 @@
Openedx Hooks Extension Framework
=================================
To sustain the growth of the Open edX ecosystem, the business rules of the
platform must be open for extension following the open-closed principle. This
framework allows developers to do just that without needing to fork and modify
the main edx-platform repository.
Context
-------
Hooks are predefined places in the edx-platform core where externally defined
functions can take place. In some cases, those functions can alter what the user
sees or experiences in the platform. Other cases are informative only. All cases
are meant to be extended using Open edX plugins and configuration.
Hooks can be of two types, events and filters. Events are in essence signals, in
that they are sent in specific application places and whose listeners can extend
functionality. On the other hand Filters are passed data and can act on it
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`_
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
to migrate to newer open releases with very little to no development effort. In
the case of the events, this is detailed in the `versioning ADR`_ and the
`payload ADR`_.
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
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
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 <https://github.com/eduNEXT/openedx-events/blob/main/openedx_events/learning/signals.py#L18>`_
- org.openedx.learning.student.registration.completed.v1
- `2021-09-02 <https://github.com/edx/edx-platform/blob/master/openedx/core/djangoapps/user_authn/views/register.py#L258>`__
* - `SESSION_LOGIN_COMPLETED <https://github.com/eduNEXT/openedx-events/blob/main/openedx_events/learning/signals.py#L30>`_
- org.openedx.learning.auth.session.login.completed.v1
- `2021-09-02 <https://github.com/edx/edx-platform/blob/master/openedx/core/djangoapps/user_authn/views/login.py#L306>`__
* - `COURSE_ENROLLMENT_CREATED <https://github.com/eduNEXT/openedx-events/blob/main/openedx_events/learning/signals.py#L42>`_
- org.openedx.learning.course.enrollment.created.v1
- `2021-09-02 <https://github.com/edx/edx-platform/blob/master/common/djangoapps/student/models.py#L1675>`__
* - `COURSE_ENROLLMENT_CHANGED <https://github.com/eduNEXT/openedx-events/blob/main/openedx_events/learning/signals.py#L54>`_
- org.openedx.learning.course.enrollment.changed.v1
- `2021-09-22 <https://github.com/edx/edx-platform/blob/master/common/djangoapps/student/models.py#L1675>`__
* - `COURSE_UNENROLLMENT_COMPLETED <https://github.com/eduNEXT/openedx-events/blob/main/openedx_events/learning/signals.py#L66>`_
- org.openedx.learning.course.unenrollment.completed.v1
- `2021-09-22 <https://github.com/edx/edx-platform/blob/master/common/djangoapps/student/models.py#L1468>`__
* - `CERTIFICATE_CREATED <https://github.com/eduNEXT/openedx-events/blob/main/openedx_events/learning/signals.py#L78>`_
- org.openedx.learning.certificate.created.v1
- `2021-09-22 <https://github.com/edx/edx-platform/blob/master/lms/djangoapps/certificates/models.py#L506>`__
* - `CERTIFICATE_CHANGED <https://github.com/eduNEXT/openedx-events/blob/main/openedx_events/learning/signals.py#L90>`_
- org.openedx.learning.certificate.changed.v1
- `2021-09-22 <https://github.com/edx/edx-platform/blob/master/lms/djangoapps/certificates/models.py#L475>`__
* - `CERTIFICATE_REVOKED <https://github.com/eduNEXT/openedx-events/blob/main/openedx_events/learning/signals.py#L102>`_
- org.openedx.learning.certificate.revoked.v1
- `2021-09-22 <https://github.com/edx/edx-platform/blob/master/lms/djangoapps/certificates/models.py#L397>`__
* - `COHORT_MEMBERSHIP_CHANGED <https://github.com/eduNEXT/openedx-events/blob/main/openedx_events/learning/signals.py#L114>`_
- org.openedx.learning.cohort_membership.changed.v1
- `2021-09-22 <https://github.com/edx/edx-platform/blob/master/openedx/core/djangoapps/course_groups/models.py#L135>`__

View File

@@ -394,6 +394,7 @@ class GeneratedCertificate(models.Model):
status=self.status,
)
# .. event_implemented_name: CERTIFICATE_REVOKED
CERTIFICATE_REVOKED.send_event(
certificate=CertificateData(
user=UserData(
@@ -472,6 +473,7 @@ class GeneratedCertificate(models.Model):
status=self.status,
)
# .. event_implemented_name: CERTIFICATE_CHANGED
CERTIFICATE_CHANGED.send_event(
certificate=CertificateData(
user=UserData(
@@ -503,6 +505,7 @@ class GeneratedCertificate(models.Model):
status=self.status,
)
# .. event_implemented_name: CERTIFICATE_CREATED
CERTIFICATE_CREATED.send_event(
certificate=CertificateData(
user=UserData(

View File

@@ -132,6 +132,7 @@ class CohortMembership(models.Model):
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
self.full_clean(validate_unique=False)
# .. event_implemented_name: COHORT_MEMBERSHIP_CHANGED
COHORT_MEMBERSHIP_CHANGED.send_event(
cohort=CohortData(
user=UserData(

View File

@@ -303,7 +303,7 @@ def _handle_successful_authentication_and_login(user, request):
request.session.set_expiry(604800 * 4)
log.debug("Setting user session expiry to 4 weeks")
# Announce user's login
# .. event_implemented_name: SESSION_LOGIN_COMPLETED
SESSION_LOGIN_COMPLETED.send_event(
user=UserData(
pii=UserPersonalData(

View File

@@ -256,6 +256,7 @@ def create_account_with_params(request, params):
# Announce registration
REGISTER_USER.send(sender=None, user=user, registration=registration)
# .. event_implemented_name: STUDENT_REGISTRATION_COMPLETED
STUDENT_REGISTRATION_COMPLETED.send_event(
user=UserData(
pii=UserPersonalData(