Skip to content

Commit

Permalink
Event enum (#8573)
Browse files Browse the repository at this point in the history
* Add enumeration for stock events

* Update function def

* Refactor build events

* Plugin events

* Update order events

* Overdue order events

* Add documentation

* Revert mkdocs.yml

* Stringify event name

* Enum cleanup

- Support python < 3.11
- Custom __str__

* Add unit tests

* Fix duplicated code

* Update unit tests

* Bump query limit

* Use proper enums in unit tests
  • Loading branch information
SchrodingersGat authored Nov 29, 2024
1 parent 390828d commit dd9a6a8
Show file tree
Hide file tree
Showing 21 changed files with 472 additions and 58 deletions.
154 changes: 145 additions & 9 deletions docs/docs/extend/plugins/event.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,151 @@ When a certain (server-side) event occurs, the background worker passes the even
{% include 'img.html' %}
{% endwith %}

## Events

Events are passed through using a string identifier, e.g. `build.completed`

The arguments (and keyword arguments) passed to the receiving function depend entirely on the type of event.

!!! info "Read the Code"
Implementing a response to a particular event requires a working knowledge of the InvenTree code base, especially related to that event being received. While the *available* events are documented here, to implement a response to a particular event you will need to read the code to understand what data is passed to the event handler.

## Generic Events

There are a number of *generic* events which are generated on certain database actions. Whenever a database object is created, updated, or deleted, a corresponding event is generated.

#### Object Created

When a new object is created in the database, an event is generated with the following event name: `<app>_<model>.created`, where `<model>` is the name of the model class (e.g. `part`, `stockitem`, etc).

The event is called with the following keywords arguments:

- `model`: The model class of the object that was created
- `id`: The primary key of the object that was created

**Example:**

A new `Part` object is created with primary key `123`, resulting in the following event being generated:

```python
trigger_event('part_part.created', model='part', id=123)
```

### Object Updated

When an object is updated in the database, an event is generated with the following event name: `<app>_<model>.saved`, where `<model>` is the name of the model class (e.g. `part`, `stockitem`, etc).

The event is called with the following keywords arguments:

- `model`: The model class of the object that was updated
- `id`: The primary key of the object that was updated

**Example:**

A `Part` object with primary key `123` is updated, resulting in the following event being generated:

```python
trigger_event('part_part.saved', model='part', id=123)
```

### Object Deleted

When an object is deleted from the database, an event is generated with the following event name: `<app>_<model>.deleted`, where `<model>` is the name of the model class (e.g. `part`, `stockitem`, etc).

The event is called with the following keywords arguments:

- `model`: The model class of the object that was deleted
- `id`: The primary key of the object that was deleted (if available)

**Example:**

A `Part` object with primary key `123` is deleted, resulting in the following event being generated:

```python
trigger_event('part_part.deleted', model='part', id=123)
```

!!! warning "Object Deleted"
Note that the event is triggered *after* the object has been deleted from the database, so the object itself is no longer available.

## Specific Events

In addition to the *generic* events listed above, there are a number of other events which are triggered by *specific* actions within the InvenTree codebase.

The available events are provided in the enumerations listed below. Note that while the names of the events are documented here, the exact arguments passed to the event handler will depend on the specific event being triggered.

### Build Events

::: build.events.BuildEvents
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_source: True
members: []

### Part Events

::: part.events.PartEvents
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_source: True
members: []

### Stock Events

::: stock.events.StockEvents
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_source: True
members: []

### Purchase Order Events

::: order.events.PurchaseOrderEvents
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_source: True
members: []

### Sales Order Events

::: order.events.SalesOrderEvents
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_source: True
members: []

### Return Order Events

::: order.events.ReturnOrderEvents
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_source: True
members: []

### Plugin Events

::: plugin.events.PluginEvents
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_source: True
members: []

## Samples

### Sample Plugin - All events

Implementing classes must at least provide a `process_event` function:
Expand All @@ -40,12 +185,3 @@ Overall this function can reduce the workload on the background workers signific
show_root_toc_entry: False
show_source: True
members: []


## Events

Events are passed through using a string identifier, e.g. `build.completed`

The arguments (and keyword arguments) passed to the receiving function depend entirely on the type of event.

Implementing a response to a particular event requires a working knowledge of the InvenTree code base, especially related to that event being received.
1 change: 1 addition & 0 deletions src/backend/InvenTree/InvenTree/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@
) # Load plugins from setup hooks in testing?

PLUGIN_TESTING_EVENTS = False # Flag if events are tested right now
PLUGIN_TESTING_EVENTS_ASYNC = False # Flag if events are tested asynchronously

PLUGIN_RETRY = get_setting(
'INVENTREE_PLUGIN_RETRY', 'PLUGIN_RETRY', 3, typecast=int
Expand Down
67 changes: 67 additions & 0 deletions src/backend/InvenTree/InvenTree/unit_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,73 @@ def getNewestMigrationFile(app, exclude_extension=True):
return newest_file


def findOffloadedTask(
task_name: str,
clear_after: bool = False,
reverse: bool = False,
matching_args=None,
matching_kwargs=None,
):
"""Find an offloaded tasks in the background worker queue.
Arguments:
task_name: The name of the task to search for
clear_after: Clear the task queue after searching
reverse: Search in reverse order (most recent first)
matching_args: List of argument names to match against
matching_kwargs: List of keyword argument names to match against
"""
from django_q.models import OrmQ

tasks = OrmQ.objects.all()

if reverse:
tasks = tasks.order_by('-pk')

task = None

for t in tasks:
if t.func() == task_name:
found = True

if matching_args:
for arg in matching_args:
if arg not in t.args():
found = False
break

if matching_kwargs:
for kwarg in matching_kwargs:
if kwarg not in t.kwargs():
found = False
break

if found:
task = t
break

if clear_after:
OrmQ.objects.all().delete()

return task


def findOffloadedEvent(
event_name: str,
clear_after: bool = False,
reverse: bool = False,
matching_kwargs=None,
):
"""Find an offloaded event in the background worker queue."""
return findOffloadedTask(
'plugin.base.event.events.register_event',
matching_args=[str(event_name)],
matching_kwargs=matching_kwargs,
clear_after=clear_after,
reverse=reverse,
)


class UserMixin:
"""Mixin to setup a user and login for tests.
Expand Down
18 changes: 18 additions & 0 deletions src/backend/InvenTree/build/events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""Event definitions and triggers for the build app."""

from generic.events import BaseEventEnum


class BuildEvents(BaseEventEnum):
"""Event enumeration for the Build app."""

# Build order events
HOLD = 'build.hold'
ISSUED = 'build.issued'
CANCELLED = 'build.cancelled'
COMPLETED = 'build.completed'
OVERDUE = 'build.overdue_build_order'

# Build output events
OUTPUT_CREATED = 'buildoutput.created'
OUTPUT_COMPLETED = 'buildoutput.completed'
15 changes: 11 additions & 4 deletions src/backend/InvenTree/build/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from build.status_codes import BuildStatus, BuildStatusGroups
from stock.status_codes import StockStatus, StockHistoryCode

from build.events import BuildEvents
from build.filters import annotate_allocated_quantity
from build.validators import generate_next_build_reference, validate_build_order_reference
from generic.states import StateTransitionMixin
Expand Down Expand Up @@ -651,7 +652,7 @@ def _action_complete(self, *args, **kwargs):
raise ValidationError(_("Failed to offload task to complete build allocations"))

# Register an event
trigger_event('build.completed', id=self.pk)
trigger_event(BuildEvents.COMPLETED, id=self.pk)

# Notify users that this build has been completed
targets = [
Expand Down Expand Up @@ -718,7 +719,7 @@ def _action_issue(self, *args, **kwargs):
self.status = BuildStatus.PRODUCTION.value
self.save()

trigger_event('build.issued', id=self.pk)
trigger_event(BuildEvents.ISSUED, id=self.pk)

@transaction.atomic
def hold_build(self):
Expand All @@ -743,7 +744,7 @@ def _action_hold(self, *args, **kwargs):
self.status = BuildStatus.ON_HOLD.value
self.save()

trigger_event('build.hold', id=self.pk)
trigger_event(BuildEvents.HOLD, id=self.pk)

@transaction.atomic
def cancel_build(self, user, **kwargs):
Expand Down Expand Up @@ -802,7 +803,7 @@ def _action_cancel(self, *args, **kwargs):
content=InvenTreeNotificationBodies.OrderCanceled
)

trigger_event('build.cancelled', id=self.pk)
trigger_event(BuildEvents.CANCELLED, id=self.pk)

@transaction.atomic
def deallocate_stock(self, build_line=None, output=None):
Expand Down Expand Up @@ -1157,6 +1158,12 @@ def complete_build_output(self, output, user, **kwargs):
deltas=deltas
)

trigger_event(
BuildEvents.OUTPUT_COMPLETED,
id=output.pk,
build_id=self.pk,
)

# Increase the completed quantity for this build
self.completed += output.quantity

Expand Down
3 changes: 2 additions & 1 deletion src/backend/InvenTree/build/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import InvenTree.helpers_model
import InvenTree.tasks
import part.models as part_models
from build.events import BuildEvents
from build.status_codes import BuildStatusGroups
from InvenTree.ready import isImportingData
from plugin.events import trigger_event
Expand Down Expand Up @@ -272,7 +273,7 @@ def notify_overdue_build_order(bo: build_models.Build):
}
}

event_name = 'build.overdue_build_order'
event_name = BuildEvents.OVERDUE

# Send a notification to the appropriate users
common.notifications.trigger_notification(
Expand Down
6 changes: 3 additions & 3 deletions src/backend/InvenTree/build/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def test_get_build_list(self):
self.assertEqual(len(response.data), 5)

# Filter query by build status
response = self.get(url, {'status': 40}, expected_code=200)
response = self.get(url, {'status': BuildStatus.COMPLETE.value}, expected_code=200)

self.assertEqual(len(response.data), 4)

Expand Down Expand Up @@ -221,10 +221,10 @@ def test_complete(self):
{
"outputs": [{"output": output.pk} for output in outputs],
"location": 1,
"status": 50, # Item requires attention
"status": StockStatus.ATTENTION.value,
},
expected_code=201,
max_query_count=450, # TODO: Try to optimize this
max_query_count=600, # TODO: Try to optimize this
)

self.assertEqual(self.build.incomplete_outputs.count(), 0)
Expand Down
Loading

0 comments on commit dd9a6a8

Please sign in to comment.