mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-11-04 07:05:41 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			436 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			436 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
"""
 | 
						|
Plugin mixin classes
 | 
						|
"""
 | 
						|
 | 
						|
import logging
 | 
						|
import json
 | 
						|
import requests
 | 
						|
 | 
						|
from django.conf.urls import url, include
 | 
						|
from django.db.utils import OperationalError, ProgrammingError
 | 
						|
 | 
						|
from plugin.models import PluginConfig, PluginSetting
 | 
						|
from plugin.urls import PLUGIN_BASE
 | 
						|
 | 
						|
 | 
						|
logger = logging.getLogger('inventree')
 | 
						|
 | 
						|
 | 
						|
class SettingsMixin:
 | 
						|
    """
 | 
						|
    Mixin that enables global settings for the plugin
 | 
						|
    """
 | 
						|
 | 
						|
    class MixinMeta:
 | 
						|
        MIXIN_NAME = 'Settings'
 | 
						|
 | 
						|
    def __init__(self):
 | 
						|
        super().__init__()
 | 
						|
        self.add_mixin('settings', 'has_settings', __class__)
 | 
						|
        self.settings = getattr(self, 'SETTINGS', {})
 | 
						|
 | 
						|
    @property
 | 
						|
    def has_settings(self):
 | 
						|
        """
 | 
						|
        Does this plugin use custom global settings
 | 
						|
        """
 | 
						|
        return bool(self.settings)
 | 
						|
 | 
						|
    def get_setting(self, key):
 | 
						|
        """
 | 
						|
        Return the 'value' of the setting associated with this plugin
 | 
						|
        """
 | 
						|
 | 
						|
        return PluginSetting.get_setting(key, plugin=self)
 | 
						|
 | 
						|
    def set_setting(self, key, value, user=None):
 | 
						|
        """
 | 
						|
        Set plugin setting value by key
 | 
						|
        """
 | 
						|
 | 
						|
        try:
 | 
						|
            plugin, _ = PluginConfig.objects.get_or_create(key=self.plugin_slug(), name=self.plugin_name())
 | 
						|
        except (OperationalError, ProgrammingError):
 | 
						|
            plugin = None
 | 
						|
 | 
						|
        if not plugin:
 | 
						|
            # Cannot find associated plugin model, return
 | 
						|
            return
 | 
						|
 | 
						|
        PluginSetting.set_setting(key, value, user, plugin=plugin)
 | 
						|
 | 
						|
 | 
						|
class ScheduleMixin:
 | 
						|
    """
 | 
						|
    Mixin that provides support for scheduled tasks.
 | 
						|
 | 
						|
    Implementing classes must provide a dict object called SCHEDULED_TASKS,
 | 
						|
    which provides information on the tasks to be scheduled.
 | 
						|
 | 
						|
    SCHEDULED_TASKS = {
 | 
						|
        # Name of the task (will be prepended with the plugin name)
 | 
						|
        'test_server': {
 | 
						|
            'func': 'myplugin.tasks.test_server',   # Python function to call (no arguments!)
 | 
						|
            'schedule': "I",                        # Schedule type (see django_q.Schedule)
 | 
						|
            'minutes': 30,                          # Number of minutes (only if schedule type = Minutes)
 | 
						|
            'repeats': 5,                           # Number of repeats (leave blank for 'forever')
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    Note: 'schedule' parameter must be one of ['I', 'H', 'D', 'W', 'M', 'Q', 'Y']
 | 
						|
    """
 | 
						|
 | 
						|
    ALLOWABLE_SCHEDULE_TYPES = ['I', 'H', 'D', 'W', 'M', 'Q', 'Y']
 | 
						|
 | 
						|
    # Override this in subclass model
 | 
						|
    SCHEDULED_TASKS = {}
 | 
						|
 | 
						|
    class MixinMeta:
 | 
						|
        MIXIN_NAME = 'Schedule'
 | 
						|
 | 
						|
    def __init__(self):
 | 
						|
        super().__init__()
 | 
						|
        self.add_mixin('schedule', 'has_scheduled_tasks', __class__)
 | 
						|
        self.scheduled_tasks = getattr(self, 'SCHEDULED_TASKS', {})
 | 
						|
 | 
						|
        self.validate_scheduled_tasks()
 | 
						|
 | 
						|
    @property
 | 
						|
    def has_scheduled_tasks(self):
 | 
						|
        return bool(self.scheduled_tasks)
 | 
						|
 | 
						|
    def validate_scheduled_tasks(self):
 | 
						|
        """
 | 
						|
        Check that the provided scheduled tasks are valid
 | 
						|
        """
 | 
						|
 | 
						|
        if not self.has_scheduled_tasks:
 | 
						|
            raise ValueError("SCHEDULED_TASKS not defined")
 | 
						|
 | 
						|
        for key, task in self.scheduled_tasks.items():
 | 
						|
 | 
						|
            if 'func' not in task:
 | 
						|
                raise ValueError(f"Task '{key}' is missing 'func' parameter")
 | 
						|
 | 
						|
            if 'schedule' not in task:
 | 
						|
                raise ValueError(f"Task '{key}' is missing 'schedule' parameter")
 | 
						|
 | 
						|
            schedule = task['schedule'].upper().strip()
 | 
						|
 | 
						|
            if schedule not in self.ALLOWABLE_SCHEDULE_TYPES:
 | 
						|
                raise ValueError(f"Task '{key}': Schedule '{schedule}' is not a valid option")
 | 
						|
 | 
						|
            # If 'minutes' is selected, it must be provided!
 | 
						|
            if schedule == 'I' and 'minutes' not in task:
 | 
						|
                raise ValueError(f"Task '{key}' is missing 'minutes' parameter")
 | 
						|
 | 
						|
    def get_task_name(self, key):
 | 
						|
        # Generate a 'unique' task name
 | 
						|
        slug = self.plugin_slug()
 | 
						|
        return f"plugin.{slug}.{key}"
 | 
						|
 | 
						|
    def get_task_names(self):
 | 
						|
        # Returns a list of all task names associated with this plugin instance
 | 
						|
        return [self.get_task_name(key) for key in self.scheduled_tasks.keys()]
 | 
						|
 | 
						|
    def register_tasks(self):
 | 
						|
        """
 | 
						|
        Register the tasks with the database
 | 
						|
        """
 | 
						|
 | 
						|
        try:
 | 
						|
            from django_q.models import Schedule
 | 
						|
 | 
						|
            for key, task in self.scheduled_tasks.items():
 | 
						|
 | 
						|
                task_name = self.get_task_name(key)
 | 
						|
 | 
						|
                # If a matching scheduled task does not exist, create it!
 | 
						|
                if not Schedule.objects.filter(name=task_name).exists():
 | 
						|
 | 
						|
                    logger.info(f"Adding scheduled task '{task_name}'")
 | 
						|
 | 
						|
                    Schedule.objects.create(
 | 
						|
                        name=task_name,
 | 
						|
                        func=task['func'],
 | 
						|
                        schedule_type=task['schedule'],
 | 
						|
                        minutes=task.get('minutes', None),
 | 
						|
                        repeats=task.get('repeats', -1),
 | 
						|
                    )
 | 
						|
        except (ProgrammingError, OperationalError):
 | 
						|
            # Database might not yet be ready
 | 
						|
            logger.warning("register_tasks failed, database not ready")
 | 
						|
 | 
						|
    def unregister_tasks(self):
 | 
						|
        """
 | 
						|
        Deregister the tasks with the database
 | 
						|
        """
 | 
						|
 | 
						|
        try:
 | 
						|
            from django_q.models import Schedule
 | 
						|
 | 
						|
            for key, task in self.scheduled_tasks.items():
 | 
						|
 | 
						|
                task_name = self.get_task_name(key)
 | 
						|
 | 
						|
                try:
 | 
						|
                    scheduled_task = Schedule.objects.get(name=task_name)
 | 
						|
                    scheduled_task.delete()
 | 
						|
                except Schedule.DoesNotExist:
 | 
						|
                    pass
 | 
						|
        except (ProgrammingError, OperationalError):
 | 
						|
            # Database might not yet be ready
 | 
						|
            logger.warning("unregister_tasks failed, database not ready")
 | 
						|
 | 
						|
 | 
						|
class EventMixin:
 | 
						|
    """
 | 
						|
    Mixin that provides support for responding to triggered events.
 | 
						|
 | 
						|
    Implementing classes must provide a "process_event" function:
 | 
						|
    """
 | 
						|
 | 
						|
    def process_event(self, event, *args, **kwargs):
 | 
						|
        # Default implementation does not do anything
 | 
						|
        raise NotImplementedError
 | 
						|
 | 
						|
    class MixinMeta:
 | 
						|
        MIXIN_NAME = 'Events'
 | 
						|
 | 
						|
    def __init__(self):
 | 
						|
        super().__init__()
 | 
						|
        self.add_mixin('events', True, __class__)
 | 
						|
 | 
						|
 | 
						|
class UrlsMixin:
 | 
						|
    """
 | 
						|
    Mixin that enables custom URLs for the plugin
 | 
						|
    """
 | 
						|
 | 
						|
    class MixinMeta:
 | 
						|
        MIXIN_NAME = 'URLs'
 | 
						|
 | 
						|
    def __init__(self):
 | 
						|
        super().__init__()
 | 
						|
        self.add_mixin('urls', 'has_urls', __class__)
 | 
						|
        self.urls = self.setup_urls()
 | 
						|
 | 
						|
    def setup_urls(self):
 | 
						|
        """
 | 
						|
        setup url endpoints for this plugin
 | 
						|
        """
 | 
						|
        return getattr(self, 'URLS', None)
 | 
						|
 | 
						|
    @property
 | 
						|
    def base_url(self):
 | 
						|
        """
 | 
						|
        returns base url for this plugin
 | 
						|
        """
 | 
						|
        return f'{PLUGIN_BASE}/{self.slug}/'
 | 
						|
 | 
						|
    @property
 | 
						|
    def internal_name(self):
 | 
						|
        """
 | 
						|
        returns the internal url pattern name
 | 
						|
        """
 | 
						|
        return f'plugin:{self.slug}:'
 | 
						|
 | 
						|
    @property
 | 
						|
    def urlpatterns(self):
 | 
						|
        """
 | 
						|
        returns the urlpatterns for this plugin
 | 
						|
        """
 | 
						|
        if self.has_urls:
 | 
						|
            return url(f'^{self.slug}/', include((self.urls, self.slug)), name=self.slug)
 | 
						|
        return None
 | 
						|
 | 
						|
    @property
 | 
						|
    def has_urls(self):
 | 
						|
        """
 | 
						|
        does this plugin use custom urls
 | 
						|
        """
 | 
						|
        return bool(self.urls)
 | 
						|
 | 
						|
 | 
						|
class NavigationMixin:
 | 
						|
    """
 | 
						|
    Mixin that enables custom navigation links with the plugin
 | 
						|
    """
 | 
						|
 | 
						|
    NAVIGATION_TAB_NAME = None
 | 
						|
    NAVIGATION_TAB_ICON = "fas fa-question"
 | 
						|
 | 
						|
    class MixinMeta:
 | 
						|
        """
 | 
						|
        meta options for this mixin
 | 
						|
        """
 | 
						|
        MIXIN_NAME = 'Navigation Links'
 | 
						|
 | 
						|
    def __init__(self):
 | 
						|
        super().__init__()
 | 
						|
        self.add_mixin('navigation', 'has_naviation', __class__)
 | 
						|
        self.navigation = self.setup_navigation()
 | 
						|
 | 
						|
    def setup_navigation(self):
 | 
						|
        """
 | 
						|
        setup navigation links for this plugin
 | 
						|
        """
 | 
						|
        nav_links = getattr(self, 'NAVIGATION', None)
 | 
						|
        if nav_links:
 | 
						|
            # check if needed values are configured
 | 
						|
            for link in nav_links:
 | 
						|
                if False in [a in link for a in ('link', 'name', )]:
 | 
						|
                    raise NotImplementedError('Wrong Link definition', link)
 | 
						|
        return nav_links
 | 
						|
 | 
						|
    @property
 | 
						|
    def has_naviation(self):
 | 
						|
        """
 | 
						|
        does this plugin define navigation elements
 | 
						|
        """
 | 
						|
        return bool(self.navigation)
 | 
						|
 | 
						|
    @property
 | 
						|
    def navigation_name(self):
 | 
						|
        """name for navigation tab"""
 | 
						|
        name = getattr(self, 'NAVIGATION_TAB_NAME', None)
 | 
						|
        if not name:
 | 
						|
            name = self.human_name
 | 
						|
        return name
 | 
						|
 | 
						|
    @property
 | 
						|
    def navigation_icon(self):
 | 
						|
        """icon for navigation tab"""
 | 
						|
        return getattr(self, 'NAVIGATION_TAB_ICON', "fas fa-question")
 | 
						|
 | 
						|
 | 
						|
class AppMixin:
 | 
						|
    """
 | 
						|
    Mixin that enables full django app functions for a plugin
 | 
						|
    """
 | 
						|
 | 
						|
    class MixinMeta:
 | 
						|
        """meta options for this mixin"""
 | 
						|
        MIXIN_NAME = 'App registration'
 | 
						|
 | 
						|
    def __init__(self):
 | 
						|
        super().__init__()
 | 
						|
        self.add_mixin('app', 'has_app', __class__)
 | 
						|
 | 
						|
    @property
 | 
						|
    def has_app(self):
 | 
						|
        """
 | 
						|
        this plugin is always an app with this plugin
 | 
						|
        """
 | 
						|
        return True
 | 
						|
 | 
						|
 | 
						|
class APICallMixin:
 | 
						|
    """
 | 
						|
    Mixin that enables easier API calls for a plugin
 | 
						|
 | 
						|
    Steps to set up:
 | 
						|
    1. Add this mixin before (left of) SettingsMixin and PluginBase
 | 
						|
    2. Add two settings for the required url and token/passowrd (use `SettingsMixin`)
 | 
						|
    3. Save the references to keys of the settings in `API_URL_SETTING` and `API_TOKEN_SETTING`
 | 
						|
    4. (Optional) Set `API_TOKEN` to the name required for the token by the external API - Defaults to `Bearer`
 | 
						|
    5. (Optional) Override the `api_url` property method if the setting needs to be extended
 | 
						|
    6. (Optional) Override `api_headers` to add extra headers (by default the token and Content-Type are contained)
 | 
						|
    7. Access the API in you plugin code via `api_call`
 | 
						|
 | 
						|
    Example:
 | 
						|
    ```
 | 
						|
    from plugin import IntegrationPluginBase
 | 
						|
    from plugin.mixins import APICallMixin, SettingsMixin
 | 
						|
 | 
						|
 | 
						|
    class SampleApiCallerPlugin(APICallMixin, SettingsMixin, IntegrationPluginBase):
 | 
						|
        '''
 | 
						|
        A small api call sample
 | 
						|
        '''
 | 
						|
        PLUGIN_NAME = "Sample API Caller"
 | 
						|
 | 
						|
        SETTINGS = {
 | 
						|
            'API_TOKEN': {
 | 
						|
                'name': 'API Token',
 | 
						|
                'protected': True,
 | 
						|
            },
 | 
						|
            'API_URL': {
 | 
						|
                'name': 'External URL',
 | 
						|
                'description': 'Where is your API located?',
 | 
						|
                'default': 'reqres.in',
 | 
						|
            },
 | 
						|
        }
 | 
						|
        API_URL_SETTING = 'API_URL'
 | 
						|
        API_TOKEN_SETTING = 'API_TOKEN'
 | 
						|
 | 
						|
        def get_external_url(self):
 | 
						|
            '''
 | 
						|
            returns data from the sample endpoint
 | 
						|
            '''
 | 
						|
            return self.api_call('api/users/2')
 | 
						|
    ```
 | 
						|
    """
 | 
						|
    API_METHOD = 'https'
 | 
						|
    API_URL_SETTING = None
 | 
						|
    API_TOKEN_SETTING = None
 | 
						|
 | 
						|
    API_TOKEN = 'Bearer'
 | 
						|
 | 
						|
    class MixinMeta:
 | 
						|
        """meta options for this mixin"""
 | 
						|
        MIXIN_NAME = 'API calls'
 | 
						|
 | 
						|
    def __init__(self):
 | 
						|
        super().__init__()
 | 
						|
        self.add_mixin('api_call', 'has_api_call', __class__)
 | 
						|
 | 
						|
    @property
 | 
						|
    def has_api_call(self):
 | 
						|
        """Is the mixin ready to call external APIs?"""
 | 
						|
        if not bool(self.API_URL_SETTING):
 | 
						|
            raise ValueError("API_URL_SETTING must be defined")
 | 
						|
        if not bool(self.API_TOKEN_SETTING):
 | 
						|
            raise ValueError("API_TOKEN_SETTING must be defined")
 | 
						|
        return True
 | 
						|
 | 
						|
    @property
 | 
						|
    def api_url(self):
 | 
						|
        return f'{self.API_METHOD}://{self.get_setting(self.API_URL_SETTING)}'
 | 
						|
 | 
						|
    @property
 | 
						|
    def api_headers(self):
 | 
						|
        return {
 | 
						|
            self.API_TOKEN: self.get_setting(self.API_TOKEN_SETTING),
 | 
						|
            'Content-Type': 'application/json'
 | 
						|
        }
 | 
						|
 | 
						|
    def api_build_url_args(self, arguments):
 | 
						|
        groups = []
 | 
						|
        for key, val in arguments.items():
 | 
						|
            groups.append(f'{key}={",".join([str(a) for a in val])}')
 | 
						|
        return f'?{"&".join(groups)}'
 | 
						|
 | 
						|
    def api_call(self, endpoint, method: str = 'GET', url_args=None, data=None, headers=None, simple_response: bool = True):
 | 
						|
        if url_args:
 | 
						|
            endpoint += self.api_build_url_args(url_args)
 | 
						|
 | 
						|
        if headers is None:
 | 
						|
            headers = self.api_headers
 | 
						|
 | 
						|
        # build kwargs for call
 | 
						|
        kwargs = {
 | 
						|
            'url': f'{self.api_url}/{endpoint}',
 | 
						|
            'headers': headers,
 | 
						|
        }
 | 
						|
        if data:
 | 
						|
            kwargs['data'] = json.dumps(data)
 | 
						|
 | 
						|
        # run command
 | 
						|
        response = requests.request(method, **kwargs)
 | 
						|
 | 
						|
        # return
 | 
						|
        if simple_response:
 | 
						|
            return response.json()
 | 
						|
        return response
 |