mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 13:15:43 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			375 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			375 lines
		
	
	
		
			11 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']
 | |
| 
 | |
|     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 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
 | |
| 
 | |
|     1. Add this mixin
 | |
|     2. Add two global settings for the required url and token/passowrd (use `GlobalSettingsMixin`)
 | |
|     3. Save the references to `API_URL_SETTING` and `API_PASSWORD_SETTING`
 | |
|     4. Set `API_TOKEN` to the name required for the token / password by the external API
 | |
|     5. (Optional) Override the `api_url` property method if some part of the APIs url is static
 | |
|     6. (Optional) Override `api_headers` to add extra headers (by default the token/password and Content-Type are contained)
 | |
|     6. Access the API in you plugin code via `api_call`
 | |
|     """
 | |
|     API_METHOD = 'https'
 | |
|     API_URL_SETTING = None
 | |
|     API_PASSWORD_SETTING = None
 | |
| 
 | |
|     API_TOKEN = 'Bearer'
 | |
| 
 | |
|     class MixinMeta:
 | |
|         """meta options for this mixin"""
 | |
|         MIXIN_NAME = 'external API usage'
 | |
| 
 | |
|     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?"""
 | |
|         # TODO check if settings are set
 | |
|         return True
 | |
| 
 | |
|     @property
 | |
|     def api_url(self):
 | |
|         return f'{self.API_METHOD}://{self.get_globalsetting(self.API_URL_SETTING)}'
 | |
| 
 | |
|     @property
 | |
|     def api_headers(self):
 | |
|         return {self.API_TOKEN: self.get_globalsetting(self.API_PASSWORD_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
 |