[plugins] allow static files for plugins (#7425)

* Add 'clear' option to 'invoke static'

* Add functions for copying static files from installed plugins

* Collect plugin static files as part of 'invoke static'

* Add 'activate' method for PluginConfig

* Run as part of `invoke plugins`
This commit is contained in:
Oliver 2024-06-10 21:05:40 +10:00 committed by GitHub
parent 66b2976d33
commit 9962d85570
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 137 additions and 13 deletions

View File

@ -0,0 +1,13 @@
"""Management command to collect plugin static files."""
from django.core.management import BaseCommand
class Command(BaseCommand):
"""Collect static files for all installed plugins."""
def handle(self, *args, **kwargs):
"""Run the management command."""
from plugin.staticfiles import collect_plugins_static_files
collect_plugins_static_files()

View File

@ -125,7 +125,7 @@ def canAppAccessDatabase(
excluded_commands.append('test')
if not allow_plugins:
excluded_commands.extend(['collectstatic'])
excluded_commands.extend(['collectstatic', 'collectplugins'])
for cmd in excluded_commands:
if cmd in sys.argv:

View File

@ -12,6 +12,7 @@ from django.utils.translation import gettext_lazy as _
import common.models
import InvenTree.models
import plugin.staticfiles
from plugin import InvenTreePlugin, registry
@ -186,6 +187,20 @@ class PluginConfig(InvenTree.models.MetadataMixin, models.Model):
return getattr(self.plugin, 'is_package', False)
def activate(self, active: bool) -> None:
"""Set the 'active' status of this plugin instance."""
from InvenTree.tasks import check_for_migrations, offload_task
if self.active == active:
return
self.active = active
self.save()
if active:
offload_task(check_for_migrations)
offload_task(plugin.staticfiles.copy_plugin_static_files, self.key)
class PluginSetting(common.models.BaseInvenTreeSetting):
"""This model represents settings for individual plugins."""

View File

@ -208,15 +208,7 @@ class PluginActivateSerializer(serializers.Serializer):
def update(self, instance, validated_data):
"""Apply the new 'active' value to the plugin instance."""
from InvenTree.tasks import check_for_migrations, offload_task
instance.active = validated_data.get('active', True)
instance.save()
if instance.active:
# A plugin has just been activated - check for database migrations
offload_task(check_for_migrations)
instance.activate(validated_data.get('active', True))
return instance

View File

@ -0,0 +1,92 @@
"""Static files management for InvenTree plugins."""
import logging
from pathlib import Path
from django.contrib.staticfiles.storage import staticfiles_storage
from plugin.registry import registry
logger = logging.getLogger('inventree')
def clear_static_dir(path, recursive=True):
"""Clear the specified directory from the 'static' output directory.
Arguments:
path: The path to the directory to clear
recursive: If True, clear the directory recursively
"""
if not staticfiles_storage.exists(path):
return
dirs, files = staticfiles_storage.listdir(path)
for f in files:
staticfiles_storage.delete(f'{path}/{f}')
if recursive:
for d in dirs:
clear_static_dir(f'{path}/{d}', recursive=True)
staticfiles_storage.delete(d)
def collect_plugins_static_files():
"""Copy static files from all installed plugins into the static directory."""
registry.check_reload()
logger.info('Collecting static files for all installed plugins.')
for slug in registry.plugins.keys():
copy_plugin_static_files(slug)
def copy_plugin_static_files(slug):
"""Copy static files for the specified plugin."""
registry.check_reload()
plugin = registry.get_plugin(slug)
if not plugin:
return
logger.info("Copying static files for plugin '%s'")
# Get the source path for the plugin
source_path = plugin.path().joinpath('static')
if not source_path.is_dir():
return
# Create prefix for the destination path
destination_prefix = f'plugins/{slug}/'
# Clear the destination path
clear_static_dir(destination_prefix)
items = list(source_path.glob('*'))
idx = 0
copied = 0
while idx < len(items):
item = items[idx]
idx += 1
if item.is_dir():
items.extend(item.glob('*'))
continue
if item.is_file():
relative_path = item.relative_to(source_path)
destination_path = f'{destination_prefix}{relative_path}'
with item.open('rb') as src:
staticfiles_storage.save(destination_path, src)
logger.debug(f'- copied {item} to {destination_path}')
copied += 1
logger.info(f"Copied %s static files for plugin '%s'.", copied, slug)

View File

@ -227,6 +227,9 @@ def plugins(c, uv=False):
c.run('pip3 install --no-cache-dir --disable-pip-version-check uv')
c.run(f"uv pip install -r '{plugin_file}'")
# Collect plugin static files
manage(c, 'collectplugins')
@task(help={'uv': 'Use UV package manager (experimental)'})
def install(c, uv=False):
@ -317,8 +320,8 @@ def remove_mfa(c, mail=''):
manage(c, f'remove_mfa {mail}')
@task(help={'frontend': 'Build the frontend'})
def static(c, frontend=False):
@task(help={'frontend': 'Build the frontend', 'clear': 'Remove existing static files'})
def static(c, frontend=False, clear=True):
"""Copies required static files to the STATIC_ROOT directory, as per Django requirements."""
manage(c, 'prerender')
@ -327,7 +330,16 @@ def static(c, frontend=False):
frontend_build(c)
print('Collecting static files...')
manage(c, 'collectstatic --no-input --clear --verbosity 0')
cmd = 'collectstatic --no-input --verbosity 0'
if clear:
cmd += ' --clear'
manage(c, cmd)
# Collect plugin static files
manage(c, 'collectplugins')
@task