2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-08-07 20:32:12 +00:00
Files
InvenTree/InvenTree/plugin/models.py
Oliver b50a6826ef Builtin plugins (#3889)
* Allow loading of "builtin" plugins, even if "plugins" are not explicitly loaded

* Updates for 'admin' buttons:

- Make them work like proper links
- Hidden if 'hide_admin_link' customization option is set
- Check for user staff status

* Cleanup rendering of "plugins" display

* Consolidate InvenTree barcode plugins into single plugin class

* Hide "install plugin" button if plugins are not enabled

* Add info message is external plugins are not enabled

* Fixes for loading plugins

- Always load 'builtin' plugins
- Refactor calls to "is_active" at various points in codebase

* Various tweaks

- Improve builtin plugin descriptions
- Spelling fixes

* Adjust plugin detail for builtin plugins

* Simplify barcode plugin class

* Simplify template rendering

* Bug fix for inventree barcode plugin

* Revert "Simplify template rendering"

This reverts commit 3a6755a659.

* Re-re-improve template rendering

- Required as the template has been refactored for both "active" and "inactive" plugins

* Fixing unit tests for barcode plugin

* Ensure that barcode scan actions do not take a "long time":

- Add a default timeout of 0.1s to any POST or GET request in the testing framework
- Can be overridden by calling method if desired

* Display plugin "builtin" status in admin panel

* Fix unit tests for plugin API

* Further unit testing fixes

* Version number tweaks

* Further tweaks for unit testing

* Allow longer timeout for report printing via API

* Increase default timeout for API tests

- Sometimes CPU spike can cause the test to fail :|

* label printing can take a bit longer

* Remove timeout requirement from API tester

- Too variable to be reliable for CI
2022-11-05 01:05:54 +11:00

271 lines
8.4 KiB
Python

"""Plugin model definitions."""
import warnings
from django.conf import settings
from django.contrib import admin
from django.contrib.auth.models import User
from django.db import models
from django.utils.translation import gettext_lazy as _
import common.models
from plugin import InvenTreePlugin, registry
class MetadataMixin(models.Model):
"""Model mixin class which adds a JSON metadata field to a model, for use by any (and all) plugins.
The intent of this mixin is to provide a metadata field on a model instance,
for plugins to read / modify as required, to store any extra information.
The assumptions for models implementing this mixin are:
- The internal InvenTree business logic will make no use of this field
- Multiple plugins may read / write to this metadata field, and not assume they have sole rights
"""
class Meta:
"""Meta for MetadataMixin."""
abstract = True
metadata = models.JSONField(
blank=True, null=True,
verbose_name=_('Plugin Metadata'),
help_text=_('JSON metadata field, for use by external plugins'),
)
def get_metadata(self, key: str, backup_value=None):
"""Finds metadata for this model instance, using the provided key for lookup.
Args:
key: String key for requesting metadata. e.g. if a plugin is accessing the metadata, the plugin slug should be used
Returns:
Python dict object containing requested metadata. If no matching metadata is found, returns None
"""
if self.metadata is None:
return backup_value
return self.metadata.get(key, backup_value)
def set_metadata(self, key: str, data, commit: bool = True):
"""Save the provided metadata under the provided key.
Args:
key (str): Key for saving metadata
data (Any): Data object to save - must be able to be rendered as a JSON string
commit (bool, optional): If true, existing metadata with the provided key will be overwritten. If false, a merge will be attempted. Defaults to True.
"""
if self.metadata is None:
# Handle a null field value
self.metadata = {}
self.metadata[key] = data
if commit:
self.save()
class PluginConfig(models.Model):
"""A PluginConfig object holds settings for plugins.
Attributes:
key: slug of the plugin (this must be unique across all installed plugins!)
name: PluginName of the plugin - serves for a manual double check if the right plugin is used
active: Should the plugin be loaded?
"""
class Meta:
"""Meta for PluginConfig."""
verbose_name = _("Plugin Configuration")
verbose_name_plural = _("Plugin Configurations")
key = models.CharField(
unique=True,
max_length=255,
verbose_name=_('Key'),
help_text=_('Key of plugin'),
)
name = models.CharField(
null=True,
blank=True,
max_length=255,
verbose_name=_('Name'),
help_text=_('PluginName of the plugin'),
)
active = models.BooleanField(
default=False,
verbose_name=_('Active'),
help_text=_('Is the plugin active'),
)
def __str__(self) -> str:
"""Nice name for printing."""
name = f'{self.name} - {self.key}'
if not self.active:
name += '(not active)'
return name
# extra attributes from the registry
def mixins(self):
"""Returns all registered mixins."""
try:
return self.plugin._mixinreg
except (AttributeError, ValueError): # pragma: no cover
return {}
# functions
def __init__(self, *args, **kwargs):
"""Override to set original state of the plugin-config instance."""
super().__init__(*args, **kwargs)
self.__org_active = self.active
# append settings from registry
plugin = registry.plugins_full.get(self.key, None)
def get_plugin_meta(name):
if plugin:
return str(getattr(plugin, name, None))
return None
self.meta = {
key: get_plugin_meta(key) for key in ['slug', 'human_name', 'description', 'author',
'pub_date', 'version', 'website', 'license',
'package_path', 'settings_url', ]
}
# Save plugin
self.plugin: InvenTreePlugin = plugin
def save(self, force_insert=False, force_update=False, *args, **kwargs):
"""Extend save method to reload plugins if the 'active' status changes."""
reload = kwargs.pop('no_reload', False) # check if no_reload flag is set
ret = super().save(force_insert, force_update, *args, **kwargs)
if not reload:
if (self.active is False and self.__org_active is True) or \
(self.active is True and self.__org_active is False):
if settings.PLUGIN_TESTING:
warnings.warn('A reload was triggered')
registry.reload_plugins()
return ret
@admin.display(boolean=True, description=_('Sample plugin'))
def is_sample(self) -> bool:
"""Is this plugin a sample app?"""
if not self.plugin:
return False
return self.plugin.check_is_sample()
@admin.display(boolean=True, description=_('Builtin Plugin'))
def is_builtin(self) -> bool:
"""Return True if this is a 'builtin' plugin"""
if not self.plugin:
return False
return self.plugin.check_is_builtin()
class PluginSetting(common.models.BaseInvenTreeSetting):
"""This model represents settings for individual plugins."""
typ = 'plugin'
class Meta:
"""Meta for PluginSetting."""
unique_together = [
('plugin', 'key'),
]
plugin = models.ForeignKey(
PluginConfig,
related_name='settings',
null=False,
verbose_name=_('Plugin'),
on_delete=models.CASCADE,
)
@classmethod
def get_setting_definition(cls, key, **kwargs):
"""In the BaseInvenTreeSetting class, we have a class attribute named 'SETTINGS', which is a dict object that fully defines all the setting parameters.
Here, unlike the BaseInvenTreeSetting, we do not know the definitions of all settings
'ahead of time' (as they are defined externally in the plugins).
Settings can be provided by the caller, as kwargs['settings'].
If not provided, we'll look at the plugin registry to see what settings are available,
(if the plugin is specified!)
"""
if 'settings' not in kwargs:
plugin = kwargs.pop('plugin', None)
if plugin:
if issubclass(plugin.__class__, InvenTreePlugin):
plugin = plugin.plugin_config()
kwargs['settings'] = registry.mixins_settings.get(plugin.key, {})
return super().get_setting_definition(key, **kwargs)
def get_kwargs(self):
"""Explicit kwargs required to uniquely identify a particular setting object, in addition to the 'key' parameter."""
return {
'plugin': self.plugin,
}
class NotificationUserSetting(common.models.BaseInvenTreeSetting):
"""This model represents notification settings for a user."""
typ = 'notification'
class Meta:
"""Meta for NotificationUserSetting."""
unique_together = [
('method', 'user', 'key'),
]
@classmethod
def get_setting_definition(cls, key, **kwargs):
"""Override setting_definition to use notification settings."""
from common.notifications import storage
kwargs['settings'] = storage.user_settings
return super().get_setting_definition(key, **kwargs)
def get_kwargs(self):
"""Explicit kwargs required to uniquely identify a particular setting object, in addition to the 'key' parameter."""
return {
'method': self.method,
'user': self.user,
}
method = models.CharField(
max_length=255,
verbose_name=_('Method'),
)
user = models.ForeignKey(
User,
on_delete=models.CASCADE,
blank=True, null=True,
verbose_name=_('User'),
help_text=_('User'),
)
def __str__(self) -> str:
"""Nice name of printing."""
return f'{self.key} (for {self.user}): {self.value}'