New Administration menu, plugins panel, changed plugins UI enable/disable workflow

pull/636/head
Piero Toffanin 2019-03-19 15:19:23 -04:00
rodzic 5d269516a3
commit e11a58739a
18 zmienionych plików z 231 dodań i 169 usunięć

Wyświetl plik

@ -1,8 +1,14 @@
from django.conf.urls import url
from django.contrib import admin
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.utils.html import format_html
from guardian.admin import GuardedModelAdmin
from app.models import PluginDatum
from app.models import Preset
from app.models import Plugin
from app.plugins import get_plugin_by_name
from .models import Project, Task, ImageUpload, Setting, Theme
from django import forms
from codemirror2.widgets import CodeMirrorEditor
@ -61,5 +67,71 @@ class ThemeAdmin(admin.ModelAdmin):
admin.site.register(Theme, ThemeAdmin)
admin.site.register(PluginDatum, admin.ModelAdmin)
admin.site.register(PluginDatum, admin.ModelAdmin)
class PluginAdmin(admin.ModelAdmin):
list_display = ("name", "description", "version", "author", "enabled", "plugin_actions")
readonly_fields = ("name", )
def has_add_permission(self, request):
return False
def has_delete_permission(self, request, obj=None):
return False
def description(self, obj):
manifest = get_plugin_by_name(obj.name, only_active=False).get_manifest()
return manifest.get('description', '')
def version(self, obj):
manifest = get_plugin_by_name(obj.name, only_active=False).get_manifest()
return manifest.get('version', '')
def author(self, obj):
manifest = get_plugin_by_name(obj.name, only_active=False).get_manifest()
return manifest.get('author', '')
def get_urls(self):
urls = super().get_urls()
print(urls)
custom_urls = [
url(
r'^(?P<plugin_name>.+)/enable/$',
self.admin_site.admin_view(self.plugin_enable),
name='plugin-enable',
),
url(
r'^(?P<plugin_name>.+)/disable/$',
self.admin_site.admin_view(self.plugin_disable),
name='plugin-disable',
),
]
return custom_urls + urls
def plugin_enable(self, request, plugin_name, *args, **kwargs):
p = Plugin.objects.get(pk=plugin_name)
p.enabled = True
p.save()
return HttpResponseRedirect(reverse('admin:app_plugin_changelist'))
def plugin_disable(self, request, plugin_name, *args, **kwargs):
p = Plugin.objects.get(pk=plugin_name)
p.enabled = False
p.save()
return HttpResponseRedirect(reverse('admin:app_plugin_changelist'))
def plugin_actions(self, obj):
return format_html(
'<a class="button" href="{}" {}>Disable</a>&nbsp;'
'<a class="button" href="{}" {}>Enable</a>',
reverse('admin:plugin-disable', args=[obj.pk]) if obj.enabled else '#',
'disabled' if not obj.enabled else '',
reverse('admin:plugin-enable', args=[obj.pk]) if not obj.enabled else '#',
'disabled' if obj.enabled else '',
)
plugin_actions.short_description = 'Actions'
plugin_actions.allow_tags = True
admin.site.register(Plugin, PluginAdmin)

Wyświetl plik

@ -11,7 +11,7 @@ from guardian.shortcuts import assign_perm
from worker import tasks as worker_tasks
from app.models import Preset
from app.models import Theme
from app.plugins import register_plugins
from app.plugins import init_plugins
from nodeodm.models import ProcessingNode
# noinspection PyUnresolvedReferences
from webodm.settings import MEDIA_ROOT
@ -81,7 +81,7 @@ def boot():
logger.info("Created settings")
register_plugins()
init_plugins()
if not settings.TESTING:
try:
@ -96,6 +96,12 @@ def boot():
def add_default_presets():
try:
Preset.objects.update_or_create(name='Volume Analysis', system=True,
defaults={'options': [{'name': 'use-opensfm-dense', 'value': True},
{'name': 'dsm', 'value': True},
{'name': 'dem-resolution', 'value': '2'},
{'name': 'depthmap-resolution', 'value': '1000'},
{'name': 'opensfm-depthmap-min-patch-sd', 'value': '0'}]})
Preset.objects.update_or_create(name='3D Model', system=True,
defaults={'options': [{'name': 'mesh-octree-depth', 'value': "11"},
{'name': 'use-3dmesh', 'value': True},

Wyświetl plik

@ -0,0 +1,20 @@
# Generated by Django 2.1.7 on 2019-03-19 16:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('app', '0026_update_images_count'),
]
operations = [
migrations.CreateModel(
name='Plugin',
fields=[
('name', models.CharField(help_text='Plugin name', max_length=255, primary_key=True, serialize=False)),
('enabled', models.BooleanField(db_index=True, default=True, help_text='Whether this plugin is enabled.')),
],
),
]

Wyświetl plik

@ -5,4 +5,5 @@ from .preset import Preset
from .theme import Theme
from .setting import Setting
from .plugin_datum import PluginDatum
from .plugin import Plugin

Wyświetl plik

@ -0,0 +1,9 @@
from django.db import models
class Plugin(models.Model):
name = models.CharField(max_length=255, primary_key=True, blank=False, null=False, help_text="Plugin name")
enabled = models.BooleanField(db_index=True, default=True, help_text="Whether this plugin is enabled.")
def __str__(self):
return self.name

Wyświetl plik

@ -11,14 +11,48 @@ from string import Template
from django.http import HttpResponse
from app.models import Plugin
from app.models import Setting
from webodm import settings
logger = logging.getLogger('app.logger')
def register_plugins():
for plugin in get_active_plugins():
def init_plugins():
build_plugins()
sync_plugin_db()
register_plugins()
def sync_plugin_db():
"""
Creates db entries for undiscovered plugins to keep track
of enabled/disabled plugins
"""
db_plugins = Plugin.objects.all()
fs_plugins = get_plugins()
# Remove plugins that are in the database but not on the file system
for db_plugin in db_plugins:
fs_found = next((fs_plugin for fs_plugin in fs_plugins if db_plugin.name == fs_plugin.get_name()), None)
if not fs_found:
Plugin.objects.filter(name=db_plugin.name).delete()
logger.info("Cleaned [{}] plugin from database (not found in file system)".format(db_plugin.name))
# Add plugins found in the file system, but not yet in the database
for plugin in get_plugins():
# Plugins that have a "disabled" file are disabled
disabled_path = plugin.get_path("disabled")
disabled = os.path.isfile(disabled_path)
if not disabled:
_, created = Plugin.objects.get_or_create(
name=plugin.get_name(),
defaults={'enabled': not disabled},
)
if created:
logger.info("Added [{}] plugin to database".format(plugin.get_name()))
def build_plugins():
for plugin in get_plugins():
# Check for package.json in public directory
# and run npm install if needed
if plugin.path_exists("public/package.json") and not plugin.path_exists("public/node_modules"):
@ -27,8 +61,6 @@ def register_plugins():
# Check if we need to generate a webpack.config.js
if len(plugin.build_jsx_components()) > 0 and plugin.path_exists('public'):
logger.info("Generating webpack.config.js for {}".format(plugin))
build_paths = map(lambda p: os.path.join(plugin.get_path('public'), p), plugin.build_jsx_components())
paths_ok = not (False in map(lambda p: os.path.exists, build_paths))
@ -48,14 +80,17 @@ def register_plugins():
with open(plugin.get_path('public/webpack.config.js'), 'w') as f:
f.write(wpc_content)
else:
logger.warning("Cannot generate webpack.config.js for {}, a path is missing: {}".format(plugin, ' '.join(build_paths)))
logger.warning(
"Cannot generate webpack.config.js for {}, a path is missing: {}".format(plugin, ' '.join(build_paths)))
# Check for webpack.config.js (if we need to build it)
if plugin.path_exists("public/webpack.config.js") and not plugin.path_exists("public/build"):
logger.info("Running webpack for {}".format(plugin.get_name()))
subprocess.call(['webpack-cli'], cwd=plugin.get_path("public"))
def register_plugins():
for plugin in get_active_plugins():
plugin.register()
logger.info("Registered {}".format(plugin))
@ -94,22 +129,23 @@ def get_api_url_patterns():
return url_patterns
plugins = None
def get_active_plugins():
def get_plugins():
"""
:return: all plugins instances (enabled or not)
"""
# Cache plugins search
global plugins
if plugins != None: return plugins
plugins = []
plugins_path = get_plugins_path()
plugins = []
for dir in [d for d in os.listdir(plugins_path) if os.path.isdir(plugins_path)]:
# Each plugin must have a manifest.json and a plugin.py
plugin_path = os.path.join(plugins_path, dir)
manifest_path = os.path.join(plugin_path, "manifest.json")
pluginpy_path = os.path.join(plugin_path, "plugin.py")
disabled_path = os.path.join(plugin_path, "disabled")
manifest_path = os.path.join(plugin_path, "manifest.json")
# Do not load test plugin unless we're in test mode
if os.path.basename(plugin_path) == 'test' and not settings.TESTING:
@ -119,37 +155,50 @@ def get_active_plugins():
if os.path.basename(plugin_path) == '.gitignore':
continue
# Check plugin required files
if not os.path.isfile(manifest_path) or not os.path.isfile(pluginpy_path):
logger.warning("Found invalid plugin in {}".format(plugin_path))
continue
# Plugins that have a "disabled" file are disabled
if os.path.isfile(disabled_path):
continue
# Instantiate the plugin
try:
module = importlib.import_module("plugins.{}".format(dir))
plugin = (getattr(module, "Plugin"))()
# Read manifest
with open(manifest_path) as manifest_file:
manifest = json.load(manifest_file)
# Check version
manifest = plugin.get_manifest()
if 'webodmMinVersion' in manifest:
min_version = manifest['webodmMinVersion']
if versionToInt(min_version) > versionToInt(settings.VERSION):
logger.warning("In {} webodmMinVersion is set to {} but WebODM version is {}. Plugin will not be loaded. Update WebODM.".format(manifest_path, min_version, settings.VERSION))
logger.warning(
"In {} webodmMinVersion is set to {} but WebODM version is {}. Plugin will not be loaded. Update WebODM.".format(
manifest_path, min_version, settings.VERSION))
continue
# Instantiate the plugin
try:
module = importlib.import_module("plugins.{}".format(dir))
cls = getattr(module, "Plugin")
plugins.append(cls())
plugins.append(plugin)
except Exception as e:
logger.warning("Failed to instantiate plugin {}: {}".format(dir, e))
return plugins
def get_plugin_by_name(name):
plugins = get_active_plugins()
def get_active_plugins():
plugins = []
enabled_plugins = [p.name for p in Plugin.objects.filter(enabled=True).all()]
for plugin in get_plugins():
if plugin.get_name() in enabled_plugins:
plugins.append(plugin)
return plugins
def get_plugin_by_name(name, only_active=True):
if only_active:
plugins = get_active_plugins()
else:
plugins = get_plugins()
res = list(filter(lambda p: p.get_name() == name, plugins))
return res[0] if res else None

Wyświetl plik

@ -1,3 +1,4 @@
import json
import logging, os, sys
from abc import ABC
from app.plugins import UserDataStore, GlobalDataStore
@ -7,6 +8,7 @@ logger = logging.getLogger('app.logger')
class PluginBase(ABC):
def __init__(self):
self.name = self.get_module_name().split(".")[-2]
self.manifest = None
def register(self):
pass
@ -130,5 +132,17 @@ class PluginBase(ABC):
from app.plugins import get_dynamic_script_handler
return get_dynamic_script_handler(self.get_path(script_path), callback, **template_args)
def get_manifest(self):
# Lazy loading
if self.manifest: return self.manifest
manifest_path = self.get_path("manifest.json")
# Read manifest
with open(manifest_path) as manifest_file:
self.manifest = json.load(manifest_file)
return self.manifest
def __str__(self):
return "[{}]".format(self.get_module_name())

Wyświetl plik

@ -9,6 +9,10 @@
width: 100%;
}
#changelist .field-plugin_actions a[disabled]{
pointer-events: none;
}
.change-list .hiddenfields { display:none; }
.change-list .filtered table {

Wyświetl plik

@ -270,22 +270,25 @@
{% if user.is_staff %}
<li>
<a href="/admin/"><i class="fa fa-gears fa-fw"></i> {% trans 'Administration' %}</a>
</li>
{% endif %}
<li>
<a href="/api/"><i class="fa fa-book fa-fw"></i> {% trans 'API' %}</a>
</li>
{% if user.is_staff %}
<li>
<a href="#"><i class="fa fa-magic fa-fw"></i> {% trans 'Customize' %}<span class="fa arrow"></span></a>
<a href="#"><i class="fa fa-gears fa-fw"></i> {% trans 'Administration' %}<span class="fa arrow"></span></a>
<ul class="nav nav-second-level">
<li>
<a href="{% url 'admin:app_setting_change' SETTINGS.id %}"><i class="fa fa-hand-o-right"></i> {% trans 'Brand' %}</a>
<a href="/admin/auth/user/"><i class="fa fa-user fa-fw"></i> {% trans 'Accounts' %}</a>
</li>
<li>
<a href="{% url 'admin:app_theme_change' SETTINGS.theme.id %}"><i class="fa fa-paint-brush"></i> {% trans 'Theme' %}</a>
<a href="/admin/auth/group/"><i class="fa fa-group fa-fw"></i> {% trans 'Groups' %}</a>
</li>
<li>
<a href="{% url 'admin:app_setting_change' SETTINGS.id %}"><i class="fa fa-magic fa-fw"></i> {% trans 'Brand' %}</a>
</li>
<li>
<a href="{% url 'admin:app_theme_change' SETTINGS.theme.id %}"><i class="fa fa-paint-brush fa-fw"></i> {% trans 'Theme' %}</a>
</li>
<li>
<a href="/admin/app/plugin/"><i class="fa fa-plug fa-fw"></i> {% trans 'Plugins' %}</a>
</li>
<li>
<a href="/admin/app/"><i class="fa fa-gear fa-fw"></i> {% trans 'Application' %}</a>
</li>
</ul>
</li>

Wyświetl plik

@ -158,6 +158,9 @@ class TestApp(BootTestCase):
admin_menu_items = ['/admin/app/setting/{}/change/'.format(settingId),
'/admin/app/theme/{}/change/'.format(themeId),
'/admin/',
'/admin/app/plugin/',
'/admin/auth/user/',
'/admin/auth/group/',
]
for url in admin_menu_items:

Wyświetl plik

@ -1,8 +0,0 @@
version: '2.1'
services:
webapp:
volumes:
- ./plugins:/webodm/plugins
worker:
volumes:
- ./plugins:/webodm/plugins

Wyświetl plik

@ -1,8 +1,8 @@
{
"name": "Diagnostic",
"webodmMinVersion": "0.6.2",
"description": "description",
"version": "0.1.0",
"description": "Display program version, memory and disk space usage statistics",
"version": "1.0.0",
"author": "Piero Toffanin",
"email": "pt@masseranolabs.com",
"repository": "https://github.com/OpenDroneMap/WebODM",

Wyświetl plik

@ -1,8 +1,8 @@
{
"name": "Lightning Network Bridge",
"webodmMinVersion": "0.7.1",
"description": "A plugin to sync accounts from webodm.net",
"version": "0.1.0",
"description": "Sync accounts from webodm.net",
"version": "0.9.0",
"author": "Piero Toffanin",
"email": "pt@masseranolabs.com",
"repository": "https://github.com/OpenDroneMap/WebODM",

Wyświetl plik

@ -1,8 +1,8 @@
{
"name": "Volume/Area/Length Measurements",
"webodmMinVersion": "0.5.0",
"description": "A plugin to compute volume, area and length measurements on Leaflet",
"version": "0.1.0",
"description": "Compute volume, area and length measurements on Leaflet",
"version": "1.0.0",
"author": "Abdelkoddouss Izem, Piero Toffanin",
"email": "pt@masseranolabs.com",
"repository": "https://github.com/OpenDroneMap/WebODM",

Wyświetl plik

@ -2,7 +2,7 @@
"name": "OpenAerialMap",
"webodmMinVersion": "0.6.0",
"description": "A plugin to upload orthophotos to OpenAerialMap",
"version": "0.1.0",
"version": "0.9.0",
"author": "Piero Toffanin",
"email": "pt@masseranolabs.com",
"repository": "https://github.com/OpenDroneMap/WebODM",

Wyświetl plik

@ -2,7 +2,7 @@
"name": "OSM Quick Editor Button",
"webodmMinVersion": "0.5.2",
"description": "A plugin to add a button for quickly opening OpenStreetMap's iD editor and setup a TMS basemap.",
"version": "0.1.1",
"version": "0.9.1",
"author": "Piero Toffanin",
"email": "pt@masseranolabs.com",
"repository": "https://github.com/OpenDroneMap/WebODM",

Wyświetl plik

@ -2,8 +2,8 @@
"name": "POSM GCP Interface",
"webodmMinVersion": "0.5.0",
"description": "A plugin to create GCP files from images",
"version": "0.1.0",
"author": "Piero Toffanin",
"version": "0.4.0",
"author": "Eric Brelsford, Piero Toffanin",
"email": "pt@masseranolabs.com",
"repository": "https://github.com/OpenDroneMap/WebODM",
"tags": ["gcp", "posm"],

111
webodm.sh
Wyświetl plik

@ -18,16 +18,6 @@ if [[ $platform = "Windows" ]]; then
export COMPOSE_CONVERT_WINDOWS_PATHS=1
fi
# Plugin commands require us to mount a docker volume
# but older version of Windows and certain macOS directory locations
# require user interaction. We will add better support for these in the near future.
plugins_volume=false
if [[ $platform = "Linux" ]]; then
plugins_volume=true
elif [[ $platform = "MacOS / OSX" ]] && [[ $(pwd) == /Users* ]]; then
plugins_volume=true
fi
load_default_node=true
dev_mode=false
@ -95,10 +85,6 @@ case $key in
shift # past argument
shift # past value
;;
--mount-plugins-volume)
plugins_volume=true
shift # past argument
;;
--no-default-node)
load_default_node=false
shift # past argument
@ -125,13 +111,6 @@ usage(){
echo " checkenv Do an environment check and install missing components"
echo " test Run the unit test suite (developers only)"
echo " resetadminpassword <new password> Reset the administrator's password to a new one. WebODM must be running when executing this command."
if [[ $plugins_volume = true ]]; then
echo ""
echo " plugin enable <plugin name> Enable a plugin"
echo " plugin disable <plugin name> Disable a plugin"
echo " plugin list List all available plugins"
echo " plugin cleanup Cleanup plugins build directories"
fi
echo ""
echo "Options:"
echo " --port <port> Set the port that WebODM should bind to (default: $DEFAULT_PORT)"
@ -145,9 +124,6 @@ usage(){
echo " --debug Enable debug for development environments (default: disabled)"
echo " --dev Enable development mode. In development mode you can make modifications to WebODM source files and changes will be reflected live. (default: disabled)"
echo " --broker Set the URL used to connect to the celery broker (default: $DEFAULT_BROKER)"
if [[ $plugins_volume = false ]]; then
echo " --mount-plugins-volume Always mount the ./plugins volume, even on unsupported platforms (developers only) (default: disabled)"
fi
exit
}
@ -260,10 +236,6 @@ start(){
echo "Will enable SSL ($method)"
fi
if [[ $plugins_volume = true ]]; then
command+=" -f docker-compose.plugins.yml"
fi
run "$command start || $command up"
}
@ -273,7 +245,6 @@ down(){
rebuild(){
run "docker-compose down --remove-orphans"
plugin_cleanup
run "rm -fr node_modules/ || sudo rm -fr node_modules/"
run "rm -fr nodeodm/external/NodeODM || sudo rm -fr nodeodm/external/NodeODM"
run "docker-compose -f docker-compose.yml -f docker-compose.build.yml build --no-cache"
@ -281,68 +252,6 @@ rebuild(){
echo -e "\033[1mDone!\033[0m You can now start WebODM by running $0 start"
}
plugin_cleanup(){
# Delete all node_modules and build directories within plugins' public/ folders
find plugins/ -type d \( -name build -o -name node_modules \) -path 'plugins/*/public/*' -exec rm -frv '{}' \;
}
plugin_list(){
plugins=$(ls plugins/ --hide test)
for plugin in $plugins; do
if [ -e "plugins/$plugin/disabled" ]; then
echo "$plugin [disabled]"
else
echo "$plugin"
fi
done
}
plugin_check(){
plugin_name="$1"
if [ ! -e "plugins/$plugin_name" ]; then
echo "Plugin $plugin_name does not exist."
exit 1
fi
}
plugin_volume_check(){
if [[ $plugins_volume = false ]]; then
path=$(realpath ./plugins)
echo "================"
echo "WARNING: Your platform does not support automatic volume mounting. If you want to enable/disable/develop plugins you need to:"
echo "1. Make sure docker can mount [$path] by modifying the docker File Sharing options"
echo "2. Pass the --mount-plugins-volume option to ./webodm.sh commands"
echo "================"
echo
fi
}
plugin_enable(){
plugin_name="$1"
plugin_check $plugin_name
plugin_volume_check
if [ -e "plugins/$plugin_name/disabled" ]; then
rm "plugins/$plugin_name/disabled"
echo "Plugin enabled. Run ./webodm.sh restart to apply the changes."
else
echo "Plugin already enabled."
fi
}
plugin_disable(){
plugin_name="$1"
plugin_check $plugin_name
plugin_volume_check
if [ ! -e "plugins/$plugin_name/disabled" ]; then
touch "plugins/$plugin_name/disabled"
echo "Plugin disabled. Run ./webodm.sh restart to apply the changes."
else
echo "Plugin already disabled."
fi
}
run_tests(){
# If in a container, we run the actual test commands
# otherwise we launch this command from the container
@ -417,26 +326,6 @@ elif [[ $1 = "test" ]]; then
run_tests
elif [[ $1 = "resetadminpassword" ]]; then
resetpassword $2
elif [[ $1 = "plugin" ]]; then
if [[ $2 = "cleanup" ]]; then
plugin_cleanup
elif [[ $2 = "list" ]]; then
plugin_list
elif [[ $2 = "enable" ]]; then
if [[ ! -z "$3" ]]; then
plugin_enable $3
else
usage
fi
elif [[ $2 = "disable" ]]; then
if [[ ! -z "$3" ]]; then
plugin_disable $3
else
usage
fi
else
usage
fi
else
usage
fi