openedx-webhooks 22.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,124 @@
1
+ """
2
+ Utilities used by Open edX Events Receivers.
3
+ """
4
+ import json
5
+ import logging
6
+ from collections.abc import MutableMapping
7
+ from typing import Any, Union
8
+
9
+ import requests
10
+ from attrs import fields, has
11
+ from opaque_keys import OpaqueKey
12
+ from xblock.fields import ScopeIds
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ SENSITIVE_FIELDS = frozenset({
17
+ 'password', 'password1', 'password2', 'old_password', 'new_password',
18
+ 'token', 'id_token', 'access_token', 'refresh_token',
19
+ 'client_secret', 'authorization', 'cookie', 'csrfmiddlewaretoken',
20
+ })
21
+
22
+
23
+ def redact_sensitive_fields(value):
24
+ """Remove credentials recursively without changing the caller's objects."""
25
+ if isinstance(value, dict):
26
+ return {
27
+ key: redact_sensitive_fields(item) for key, item in value.items()
28
+ if not isinstance(key, str) or key.lower() not in SENSITIVE_FIELDS
29
+ }
30
+ if isinstance(value, (list, tuple)):
31
+ return [redact_sensitive_fields(item) for item in value]
32
+ return value
33
+
34
+
35
+ def send(url, payload, www_form_urlencoded: bool = False):
36
+ """
37
+ Dispatch the payload to the webhook url, return the response and catch exceptions.
38
+ """
39
+ payload = redact_sensitive_fields(payload)
40
+ if www_form_urlencoded:
41
+ headers = {'Content-type': 'application/x-www-form-urlencoded', 'Accept': 'text/plain'}
42
+ payload = flatten_dict(payload)
43
+ data = payload
44
+ else:
45
+ headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
46
+ data = json.dumps(payload, default=str)
47
+
48
+ r = requests.post(url, data=data, headers=headers, timeout=10)
49
+
50
+ return r
51
+
52
+
53
+ def flatten_dict(dictionary, parent_key="", sep="_"):
54
+ """
55
+ Generate a flatten dictionary-like object.
56
+
57
+ Taken from:
58
+ https://stackoverflow.com/a/6027615/16823624
59
+ """
60
+ items = []
61
+ for key, value in dictionary.items():
62
+ key = str(key)
63
+ new_key = parent_key + sep + key if parent_key else key
64
+ if isinstance(value, MutableMapping):
65
+ items.extend(flatten_dict(value, new_key, sep=sep).items())
66
+ else:
67
+ items.append((new_key, str(value)))
68
+ return dict(items)
69
+
70
+
71
+ def value_serializer(inst, field, value): # pylint: disable=unused-argument
72
+ """
73
+ Serialize values for attr function.
74
+ """
75
+ return object_serializer(value)
76
+
77
+
78
+ def scope_ids_serializer(o):
79
+ """
80
+ Serialize instances of ScopeId.
81
+ """
82
+ return {
83
+ "block_type": o.block_type,
84
+ "def_id": str(o.def_id),
85
+ "usage_id": str(o.usage_id),
86
+ "user_id": o.user_id,
87
+ }
88
+
89
+
90
+ def object_serializer(o, depth=0) -> Union[dict, Any]:
91
+ """
92
+ Serialize an arbitrary object as a json-serializable dict.
93
+ """
94
+ if depth > 15:
95
+ return "! Depth limit reached !"
96
+ # First serialize scalar objects
97
+ if isinstance(o, (int, float, str)) or o is None:
98
+ return o
99
+ # ScopeIds is a class present in block structures
100
+ elif isinstance(o, ScopeIds):
101
+ return scope_ids_serializer(o)
102
+ elif isinstance(o, OpaqueKey):
103
+ return str(o)
104
+ if isinstance(o, (list, tuple, set)):
105
+ return [object_serializer(item, depth + 1) for item in o]
106
+ # Now serialize dict-like objects
107
+ return_value = {}
108
+ if isinstance(o, dict):
109
+ dict_values = o.copy()
110
+ elif has(type(o)):
111
+ dict_values = {field.name: getattr(o, field.name) for field in fields(type(o))}
112
+ elif hasattr(o, "__dict__"):
113
+ dict_values = o.__dict__.copy()
114
+ # if it is not a dict and cannot be converted to a dict, try to stringify it.
115
+ elif hasattr(o, "__str__"):
116
+ return str(o)
117
+ else:
118
+ return f"Unserializable {type(o)}"
119
+ for key, value in dict_values.items():
120
+ key = str(key)
121
+ # Hide private fields and credentials, including on nested objects.
122
+ if not key.startswith("_") and key.lower() not in SENSITIVE_FIELDS:
123
+ return_value[key] = object_serializer(value, depth + 1)
124
+ return return_value
@@ -0,0 +1,415 @@
1
+ Metadata-Version: 2.4
2
+ Name: openedx_webhooks
3
+ Version: 22.0.0
4
+ Summary: Webhooks for Open edX
5
+ Home-page: https://github.com/aulasneo/openedx-webhooks
6
+ Author: Aulasneo
7
+ Author-email: andres@aulasneo.com
8
+ License: AGPL 3.0
9
+ Keywords: Openedx webhooks
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Framework :: Django
12
+ Classifier: Framework :: Django :: 5.2
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)
15
+ Classifier: Natural Language :: English
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Requires-Python: >=3.12
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE.txt
21
+ Requires-Dist: Django<5.3,>=5.2
22
+ Requires-Dist: attrs
23
+ Requires-Dist: django-model-utils
24
+ Requires-Dist: edx_opaque_keys
25
+ Requires-Dist: openedx-events<12,>=11.2.0
26
+ Requires-Dist: openedx-filters<4,>=3.4.1
27
+ Requires-Dist: requests
28
+ Requires-Dist: xblock
29
+ Dynamic: author
30
+ Dynamic: author-email
31
+ Dynamic: classifier
32
+ Dynamic: description
33
+ Dynamic: description-content-type
34
+ Dynamic: home-page
35
+ Dynamic: keywords
36
+ Dynamic: license
37
+ Dynamic: license-file
38
+ Dynamic: requires-dist
39
+ Dynamic: requires-python
40
+ Dynamic: summary
41
+
42
+ # Open edX Webhooks
43
+
44
+ [![PyPI](https://img.shields.io/pypi/v/openedx-webhooks.svg)](https://pypi.python.org/pypi/openedx-webhooks/)
45
+ [![CI](https://github.com/aulasneo/openedx-webhooks/workflows/Python%20CI/badge.svg?branch=main)](https://github.com/aulasneo/openedx-webhooks/actions)
46
+ [![Codecov](https://codecov.io/github/aulasneo/openedx-webhooks/coverage.svg?branch=main)](https://codecov.io/github/aulasneo/openedx-webhooks?branch=main)
47
+ [![Documentation](https://readthedocs.org/projects/openedx-webhooks/badge/?version=latest)](https://github.com/aulasneo/openedx-webhooks)
48
+ [![Supported Python versions](https://img.shields.io/pypi/pyversions/openedx-webhooks.svg)](https://pypi.python.org/pypi/openedx-webhooks/)
49
+ [![License](https://img.shields.io/github/license/aulasneo/openedx-webhooks.svg)](https://github.com/aulasneo/openedx-webhooks/blob/main/LICENSE.txt)
50
+ ![Status](https://img.shields.io/badge/Status-Maintained-brightgreen)
51
+
52
+ ## Purpose
53
+
54
+ Webhooks for Open edX.
55
+
56
+ Current compatibility target: Open edX Verawood / Tutor 22.x.
57
+
58
+ This plugin implements a generic case of event handling that triggers a request
59
+ to a configurable URL when a signal is received.
60
+
61
+ ## Getting Started
62
+
63
+ ### Introduction
64
+
65
+ A `Webhook` triggers an HTTP POST request to a configurable URL when certain
66
+ events happen in the platform, including information relevant to the event. For
67
+ example, you can make the platform call an API when a user logs in, including
68
+ the user ID and email to connect to another application.
69
+
70
+ A `Webfilter` is a special case of webhook that can also modify data and/or
71
+ interrupt the process. For example, after the user login event you can update
72
+ the user full name or prevent the user from logging in.
73
+
74
+ ### Deploying
75
+
76
+ To install this plugin into an Open edX instance installed by Tutor, add this
77
+ line to the `OPENEDX_EXTRA_PIP_REQUIREMENTS` list in `config.yml`.
78
+
79
+ ```yaml
80
+ OPENEDX_EXTRA_PIP_REQUIREMENTS:
81
+ - openedx-webhooks
82
+ ```
83
+
84
+ This release targets Open edX Verawood and Tutor 22.x, which use Python 3.12.
85
+
86
+ After rebuilding the Open edX image with the updated package, run migrations
87
+ before restarting LMS, Studio, and their workers:
88
+
89
+ ```bash
90
+ tutor {dev|local|k8s} exec lms ./manage.py lms migrate
91
+ ```
92
+
93
+ This plugin now registers extension points in both LMS and CMS. Webhook and
94
+ webfilter configuration is still managed through Django admin and the same
95
+ database tables.
96
+
97
+ Migration `0008_verawood` adds the current event choices and corrects ten old
98
+ filter names that did not match their handlers. Existing URLs and controls are
99
+ preserved. Corrected configurations that previously never fired will now become
100
+ active; review enabled endpoints before upgrading. The migration does not convert
101
+ legacy instructor dashboard customizations into MFE tabs. Keep package version 21
102
+ for Ulmo deployments.
103
+
104
+ ## Configuring
105
+
106
+ A new section named `OPENEDX_WEBHOOKS` is available in the LMS Django admin
107
+ site. It contains two subsections: `Webhooks` and `Webfilters`. Add a new
108
+ webhook to define the URLs that will be called after each event is received.
109
+ More than one URL can be configured for each event. In that case, all URLs are
110
+ called.
111
+
112
+ ### Configuring Webhooks
113
+
114
+ The `Webhooks` Django admin panel has the following settings:
115
+
116
+ - Event: Choose from the list the event that will trigger the webhook.
117
+ - Webhook URL: URL to call. Get it from your webhook processor.
118
+ - Enabled: Click to enable the webhook.
119
+ - Use WWW form encoding: When enabled, the data will be passed in web form
120
+ format. If disabled, data will be passed in JSON format.
121
+
122
+ ### Configuring Webfilters
123
+
124
+ The `Webfilters` Django admin panel has the following settings:
125
+
126
+ - Description: Add a description for this webhook for reference.
127
+ - Event: Choose from the list the event that will trigger the webhook.
128
+ - Webhook URL: URL to call. Get it from your webhook processor.
129
+ - Enabled: Click to enable the webhook.
130
+ - Disable filtering: If enabled, the data passed to the web filter will not be
131
+ modified even if the response includes any update.
132
+ - Disable halting: If enabled, the process will not be interrupted even if the
133
+ response includes an exception setting.
134
+ - Halt on 4xx: Interrupt the process if the call to the URL returns any 4xx
135
+ error code.
136
+ - Redirect on 4xx: Include a URL to redirect in case of a 4xx response, if the
137
+ event supports redirection.
138
+ - Halt on 5xx: Interrupt the process if the call to the URL returns any 5xx
139
+ error code.
140
+ - Redirect on 5xx: Include a URL to redirect in case of a 5xx response, if the
141
+ event supports redirection.
142
+ - Halt on request exception: Interrupt the process if the call to the URL
143
+ results in a connection error such as a timeout.
144
+ - Redirect on request exception: Include a URL to redirect in case of a
145
+ connection error, if the event supports redirection.
146
+ - Use WWW form encoding: When enabled, the data will be passed in web form
147
+ format. If disabled, data will be passed in JSON format.
148
+
149
+ ## Receiving Data
150
+
151
+ Both webhooks and webfilters trigger POST requests to the configured URL. The
152
+ payload includes data relevant to the event that triggered the call. In all
153
+ cases, the payload includes an `event_metadata` key with at least the event
154
+ type and the date and time in UTC format. Other keys depend on the event. For
155
+ example, login events usually include `user` and `profile` keys with details of
156
+ the user logging in.
157
+
158
+ Signal webhooks retain their existing envelope: the data key is the fully
159
+ qualified upstream data class, such as
160
+ `openedx_events.authz.data.RoleAssignmentData`, alongside `event_metadata`.
161
+ Webfilters use argument-specific keys such as `user` and `profile`.
162
+
163
+ Credential fields (including passwords and password hashes) are omitted from
164
+ outgoing payloads. Remote model updates cannot change primary keys, relationships,
165
+ passwords, or account privilege flags. Other personal data is intentionally sent
166
+ to configured endpoints; restrict configuration access to trusted administrators.
167
+
168
+ If the `Use WWW form encoding` option is enabled, the data is passed as plain
169
+ key-value pairs in form encoding. The structure is flattened and the key names
170
+ are concatenated. For example, a login event includes `user_id`,
171
+ `user_email`, `event_metadata_time`, and similar fields.
172
+
173
+ ## Responding to Webfilters
174
+
175
+ The webhook processor should respond to a webfilter with a JSON structure and a
176
+ successful status code (`200`). The response can be empty or can have one or
177
+ both of these keys:
178
+
179
+ - `data`
180
+ - `exception`
181
+
182
+ ### Updating Data
183
+
184
+ The corresponding objects are updated with the values returned inside the
185
+ `data` key. Only keys present in the response are updated. Other keys remain
186
+ unchanged in the original data structures. To disable data updates, set
187
+ `Disable Filtering` in the webfilter configuration in the Django admin panel.
188
+
189
+ For example, to change the full name of a user at registration time, respond to
190
+ the `Student Registration Requested` webfilter with this data:
191
+
192
+ ```json
193
+ {
194
+ "data": {
195
+ "form_data": {
196
+ "name": "New Name"
197
+ }
198
+ }
199
+ }
200
+ ```
201
+
202
+ ### Interrupting Execution
203
+
204
+ To stop the process, add a JSON object as the value for the `exception` key.
205
+ This object must have only one key-value pair: the key is the name of the
206
+ exception to raise, and the value can be either a string representing the
207
+ message to show or another JSON object with more data.
208
+
209
+ For example, to prevent a user from registering, respond to the `Student
210
+ Registration Requested` webfilter with this data:
211
+
212
+ ```json
213
+ {
214
+ "exception": {
215
+ "PreventRegistration": "Not allowed to register"
216
+ }
217
+ }
218
+ ```
219
+
220
+ To prevent a webfilter from stopping the process, set `Disable halting` in the
221
+ webfilter configuration in the Django admin panel.
222
+
223
+ Check each function documentation to see the list of available values and
224
+ exceptions.
225
+
226
+ ## Handling Multiple Events
227
+
228
+ If you set more than one webhook or webfilter for the same event, all of them
229
+ are triggered. The responses of all webfilters are combined into one data
230
+ structure and used to update the objects. If more than one webfilter processor
231
+ includes data for the same key, the last one overrides the previous ones.
232
+
233
+ Endpoints run in ascending configuration ID order. Signal delivery failures are
234
+ logged and delivery continues to the other endpoints. Delivery is synchronous,
235
+ with ten-second connection and response-read timeouts, and has no persistent
236
+ retries. These timeouts are not a total request deadline; an endpoint that keeps
237
+ sending data can block delivery for longer than ten seconds. Webfilters
238
+ retain their configured halting behavior; filters with no upstream exception
239
+ cannot halt through the transport-error controls.
240
+
241
+ ## Developing
242
+
243
+ More information about available signals can be found in the [events
244
+ documentation](https://docs.openedx.org/projects/openedx-events/en/stable/reference/events.html)
245
+ and the [filters
246
+ documentation](https://docs.openedx.org/projects/openedx-filters/en/stable/reference/filters.html).
247
+
248
+ Verawood compatibility in this package is aligned with:
249
+
250
+ - `openedx-filters==3.4.1`
251
+ - `openedx-events==11.2.0`
252
+
253
+ The runtime is Python 3.12 with Django 5.2; tests pin Django 5.2.13 from the
254
+ Verawood platform requirements.
255
+
256
+ New filters are `InstructorDashboardTabsRequested`,
257
+ `AccountSettingsReadOnlyFieldsRequested`, and `GradeEventContextRequested`.
258
+ New `ROLE_ASSIGNMENT_CREATED` and `ROLE_ASSIGNMENT_DELETED` webhook events are
259
+ registered in both LMS and Studio. Existing learning and authoring signal names
260
+ and receiver arguments are unchanged. Discussion payloads now include `enabled`;
261
+ forum IDs are strings and several forum fields may be null.
262
+
263
+ For the new instructor dashboard, configure `InstructorDashboardTabsRequested`
264
+ and return `{"data": {"tabs": [{"tab_id": "reports", "title": "Reports", "url": "https://example.com/reports"}]}}`.
265
+ This replaces the tabs list (including with an empty list); include any existing
266
+ tabs you wish to keep. Its filter identifier is
267
+ `org.openedx.learning.instructor.dashboard.tabs.requested.v1` in the pinned source,
268
+ despite the `tabs.generated.v1` identifier mentioned in the release notes.
269
+ Legacy `InstructorDashboardRenderStarted` remains available only when the legacy
270
+ dashboard is enabled. Frontend plugin configuration is still needed to render
271
+ custom content inside the MFE.
272
+
273
+ For account restrictions, return `{"data": {"readonly_fields": ["name", "email"]}}`.
274
+ These names are added to existing restrictions; the pipeline retains a Python set.
275
+ For grade event enrichment, return `{"data": {"context": {"source": "crm"}}}`.
276
+ The context is merged while retaining the original user and course identifiers.
277
+ The pinned platform has no caller for `GradeEventContextRequested`, so it requires
278
+ an upstream/custom integration to fire. `AccountSettingsRenderStarted` and
279
+ `CourseEnrollmentQuerysetRequested` likewise have no callers in stock Verawood;
280
+ they remain registered for compatibility with custom integrations.
281
+
282
+ See the [Verawood compatibility and code review](docs/verawood-review.md) for
283
+ the upstream comparison, fixed defects, and deployment validation limits.
284
+
285
+ Existing recent coverage retained from prior releases includes:
286
+
287
+ - Filter: `AccountSettingsRenderStarted`
288
+ - Signals:
289
+ - `EXTERNAL_GRADER_SCORE_SUBMITTED`
290
+ - `LIBRARY_BLOCK_PUBLISHED`
291
+ - `LIBRARY_CONTAINER_CREATED`
292
+ - `LIBRARY_CONTAINER_UPDATED`
293
+ - `LIBRARY_CONTAINER_DELETED`
294
+ - `LIBRARY_CONTAINER_PUBLISHED`
295
+ - `COURSE_IMPORT_COMPLETED`
296
+
297
+ ### Adding More Filters
298
+
299
+ From version to version, new filters are added to Open edX. The complete list
300
+ of filters and their definitions can be found in `filters.py` in the
301
+ [openedx-filters repository](https://github.com/openedx/openedx-filters/blob/v3.4.1/openedx_filters/learning/filters.py).
302
+
303
+ To add a new filter, create the filter class handler in `filters.py`
304
+ containing the `run_filter` function. Then add a block in `common.py` linking
305
+ the signal identifier with the function. `run_filter` must always return a
306
+ dict, which can be empty.
307
+
308
+ ### Adding New Event Hooks
309
+
310
+ From version to version, new event producers are added to Open edX. The
311
+ complete list of events and their definitions can be found in `signals.py` in
312
+ different folders in the
313
+ [openedx-events repository](https://github.com/openedx/openedx-events/tree/v11.2.0/openedx_events),
314
+ depending on their category.
315
+
316
+ To add a new event hook, add the signal to the `signals` dict in `apps.py`.
317
+ Then add the corresponding block to `receivers.py`.
318
+
319
+ ### One-Time Setup
320
+
321
+ ```bash
322
+ # Clone the repository
323
+ git clone git@github.com:aulasneo/openedx-webhooks.git
324
+ cd openedx-webhooks
325
+
326
+ # Set up a virtualenv with the same name as the repo and activate it
327
+ # Here's how you might do that if you have virtualenvwrapper setup.
328
+ mkvirtualenv -p python3.12 openedx-webhooks
329
+ ```
330
+
331
+ ### Every Time You Develop Something In This Repo
332
+
333
+ ```bash
334
+ # Activate the virtualenv
335
+ # Here's how you might do that if you're using virtualenvwrapper.
336
+ workon openedx-webhooks
337
+
338
+ # Grab the latest code
339
+ git checkout main
340
+ git pull
341
+
342
+ # Install/update the dev requirements
343
+ make requirements
344
+
345
+ # Run the tests and quality checks (to verify the status before you make any changes)
346
+ make validate
347
+
348
+ # Make a new branch for your changes
349
+ git checkout -b <your_github_username>/<short_description>
350
+
351
+ # Using your favorite editor, edit the code to make your change.
352
+ vim ...
353
+
354
+ # Run your new tests
355
+ pytest ./path/to/new/tests
356
+
357
+ # Run all the tests and quality checks
358
+ make validate
359
+
360
+ # Commit all your changes
361
+ git commit ...
362
+ git push
363
+
364
+ # Open a PR and ask for review.
365
+ ```
366
+
367
+ ## Releasing
368
+
369
+ PyPI publishing uses GitHub Actions with PyPI trusted publishing. The release
370
+ workflow no longer uses a long-lived PyPI API token. Configure the project's
371
+ trusted publisher in PyPI before cutting a GitHub release.
372
+
373
+ Release automation expects tags in the format `release/v<version>`, for example
374
+ `release/v22.0.0`. Pushing one of those tags from a commit contained in `main`
375
+ creates a GitHub release, and the publish workflow can also be run manually
376
+ from the Actions tab when needed.
377
+
378
+ ## Getting Help
379
+
380
+ If you need any help, email [info@aulasneo.com](mailto:info@aulasneo.com).
381
+
382
+ ## More Help
383
+
384
+ If you're having trouble, there are discussion forums at
385
+ <https://discuss.openedx.org> where you can connect with others in the
386
+ community.
387
+
388
+ Real-time conversations are on Slack. You can request a [Slack
389
+ invitation](https://openedx.org/slack), then join the [community Slack
390
+ workspace](https://openedx.slack.com/).
391
+
392
+ For anything non-trivial, the best path is to open an issue in this repository
393
+ with as many details about the issue as possible:
394
+
395
+ <https://github.com/aulasneo/openedx-webhooks/issues>
396
+
397
+ For more information about these options, see the [Getting
398
+ Help](https://openedx.org/getting-help) page.
399
+
400
+ ## License
401
+
402
+ The code in this repository is licensed under the AGPL 3.0 unless otherwise
403
+ noted.
404
+
405
+ See [LICENSE.txt](LICENSE.txt) for details.
406
+
407
+ ## Contributing
408
+
409
+ Contributions are very welcome. If you want to contribute to this project,
410
+ please feel free to open an issue.
411
+
412
+ ## Reporting Security Issues
413
+
414
+ Please do not report security issues in public. Email
415
+ [operations@aulasneo.com](mailto:operations@aulasneo.com).
@@ -0,0 +1,27 @@
1
+ openedx_webhooks/__init__.py,sha256=zTVzmaTQLlZKPuQuheztWCOK4MZ4vd-q2ljUlY5FLUI,148
2
+ openedx_webhooks/admin.py,sha256=aCECzm0pw-nHauP80QSBWIFE6RWgjUb7raHxJQJgmgE,846
3
+ openedx_webhooks/apps.py,sha256=mOdSBRW-ZAi3VrCvQ8ZwfhZf2pInGWgh0sSYxRRf1Hk,4064
4
+ openedx_webhooks/filters.py,sha256=XZF8zkY2FVeJmmS0QwNqY27CX2C9wCb0E4m8nR5LpM0,79911
5
+ openedx_webhooks/models.py,sha256=45DZNCcGcGG92gLBYMGS5Cm2pyJbQSWyiux9-DIGWDE,5109
6
+ openedx_webhooks/receivers.py,sha256=ZJ57aaJ9Bx9MwuZ-MNQfKjfg1C1LntGLsbNuGO65wzQ,15589
7
+ openedx_webhooks/urls.py,sha256=viSWPpXc_TE9KcAjgJW4De6m26ficeMyDGbZmKRL4CE,325
8
+ openedx_webhooks/utils.py,sha256=XUxxSYiK3X2_g4zE26unDqZOY-qPWBm6_FNT2Pnzz_M,3959
9
+ openedx_webhooks/migrations/0001_initial.py,sha256=oVTL4tXM6SNWA2nlPaRbuPOsybMCxeS9GpL-VMK3R0o,1737
10
+ openedx_webhooks/migrations/0002_webfilter.py,sha256=mNLlME_gAT9yWN-9t5BunPTk5XmvqRqIVuZBVysRdCE,3228
11
+ openedx_webhooks/migrations/0003_auto_20230607_1246.py,sha256=VgVCO3me9Fx3Hu89ev97YnxrZfhbZwftZ8KXsgTYHUE,796
12
+ openedx_webhooks/migrations/0004_auto_20230613_1033.py,sha256=oIQTd1T4LqVBmNCWZKuKqNzhp0hKg0AO7lcJjmmMZmc,1094
13
+ openedx_webhooks/migrations/0005_alter_webfilter_event.py,sha256=CRCMjQKFmnupjw_FGHiMoI44_kRFfB685dl1XH-kE4E,446
14
+ openedx_webhooks/migrations/0006_alter_webfilter_event.py,sha256=fYMTYMzvnjsCuJ7tl2pRtA-YzOKYn2m8_hxegYWVOOM,1199
15
+ openedx_webhooks/migrations/0007_alter_webfilter_event.py,sha256=WlFuUGwywa41tRPFCxmdNxKABEZHf8S0MIMtrJ-VhQk,1050
16
+ openedx_webhooks/migrations/0008_verawood.py,sha256=ZswxHmnDGkTP-9Bc6mik5r95F04S28OexIqdsyp8ufo,7534
17
+ openedx_webhooks/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
+ openedx_webhooks/settings/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
+ openedx_webhooks/settings/cms.py,sha256=QEtiDk1nHq6_Quk7E7jap3GH9XWFD4zdZI4qctRU7K8,478
20
+ openedx_webhooks/settings/common.py,sha256=H06aCvRa4quxbe-I70f9354ZF24i-jbX1uX5uUxu2OM,7098
21
+ openedx_webhooks/settings/test.py,sha256=nJXOG4XNMs9VRdHmnL4TUeuONwDXGsZv5hWRSz-eU38,246
22
+ openedx_webhooks-22.0.0.dist-info/licenses/LICENSE.txt,sha256=QTW2QN7q3XszgUAXm9Dzgtu5LXYKbR1SGnqMa7ufEuY,35139
23
+ openedx_webhooks-22.0.0.dist-info/METADATA,sha256=DhAxmvWKglX1TNnhskvmrLnw1qFeLBKu1qoZhN31U4Y,16244
24
+ openedx_webhooks-22.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
25
+ openedx_webhooks-22.0.0.dist-info/entry_points.txt,sha256=kuvSNrY3saVbifd-sYukNPq5XP7vfLeUHqTKMV-Vq3k,129
26
+ openedx_webhooks-22.0.0.dist-info/top_level.txt,sha256=LLj81pdmwEFx-XJ-kcfekUK66Hi43GDz7GeANr_PfWo,17
27
+ openedx_webhooks-22.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,5 @@
1
+ [cms.djangoapp]
2
+ webhooks = openedx_webhooks.apps:WebhooksConfig
3
+
4
+ [lms.djangoapp]
5
+ webhooks = openedx_webhooks.apps:WebhooksConfig