Plugin menu entry hook, posm-gcpi plugin skeleton, mount points

pull/384/head
Piero Toffanin 2018-02-23 17:48:32 -05:00
rodzic 1bfcf80cb7
commit f97d4c1813
52 zmienionych plików z 213 dodań i 337 usunięć

Wyświetl plik

@ -1,2 +1,4 @@
from .plugin_base import PluginBase
from .menu import Menu
from .mountpoint import MountPoint
from .functions import *

Wyświetl plik

@ -23,10 +23,18 @@ def get_url_patterns():
"""
url_patterns = []
for plugin in get_active_plugins():
for mount_point in plugin.mount_points():
url_patterns.append(url('^plugins/{}/{}'.format(plugin.get_name(), mount_point.url),
mount_point.view,
*mount_point.args,
**mount_point.kwargs))
if plugin.has_public_path():
url_patterns.append(url('^plugins/{}/(.*)'.format(plugin.get_name()),
django.views.static.serve,
{'document_root': plugin.get_path("public")}))
return url_patterns
plugins = None

Wyświetl plik

@ -0,0 +1,22 @@
class Menu:
def __init__(self, label, link = "javascript:void(0)", css_icon = 'fa fa-caret-right fa-fw', submenu = []):
"""
Create a menu
:param label: text shown in entry
:param css_icon: class used for showing an icon (for example, "fa fa-wrench")
:param link: link of entry (use "#" or "javascript:void(0);" for no action)
:param submenu: list of Menu items
"""
super().__init__()
self.label = label
self.css_icon = css_icon
self.link = link
self.submenu = submenu
if (self.has_submenu()):
self.link = "#"
def has_submenu(self):
return len(self.submenu) > 0

Wyświetl plik

@ -0,0 +1,17 @@
import re
class MountPoint:
def __init__(self, url, view, *args, **kwargs):
"""
:param url: path to mount this view to, relative to plugins directory
:param view: Django view
:param args: extra args to pass to url()
:param kwargs: extra kwargs to pass to url()
"""
super().__init__()
self.url = re.sub(r'^/+', '', url) # remove leading slashes
self.view = view
self.args = args
self.kwargs = kwargs

Wyświetl plik

@ -24,10 +24,13 @@ class PluginBase(ABC):
return self.__class__.__module__
def get_include_js_urls(self):
return ["/plugins/{}/{}".format(self.get_name(), js_file) for js_file in self.include_js_files()]
return [self.url(js_file) for js_file in self.include_js_files()]
def get_include_css_urls(self):
return ["/plugins/{}/{}".format(self.get_name(), css_file) for css_file in self.include_css_files()]
return [self.url(css_file) for css_file in self.include_css_files()]
def url(self, path):
return "/plugins/{}/{}".format(self.get_name(), path)
def has_public_path(self):
return os.path.isdir(self.get_path("public"))
@ -48,5 +51,21 @@ class PluginBase(ABC):
"""
return []
def main_menu(self):
"""
Should be overriden by plugins that want to add
items to the side menu.
:return: [] of Menu objects
"""
return []
def mount_points(self):
"""
Should be overriden by plugins that want to connect
custom Django views
:return: [] of MountPoint objects
"""
return []
def __str__(self):
return "[{}]".format(self.get_module_name())

Wyświetl plik

@ -1,329 +0,0 @@
const env = {};
(function (environment) {
/**
* List of existings modules
* @type {Object}
*/
var modules = {};
/**
* Array of waiting modules
* @type {Array}
*/
var waitingModules = [];
/**
* Count created script for control
* @type {Number}
*/
var scriptCounter = 1;
/**
* Base element check for IE 6-8
* @type {Node}
*/
var baseElement = document.getElementsByTagName('base')[0];
/**
* Head element
* @type {Node}
*/
var head = document.getElementsByTagName('head')[0];
/**
* @param {String} name the name of the module
* @param {Array} deps dependencies of the module
* @param {Function} module module definition
* @param {String} dir relative dir path from which to load files
*/
function Include (name, deps, module, dir) {
var self = this;
if (typeof name !== "string") {
module = deps;
deps = name;
name = null;
}
if (deps.constructor !== [].constructor) {
module = deps;
deps = [];
}
waitingModules.unshift([name, deps, module]);
/**
* Uid for script differentiation
* @type {String}
*/
self.uid = Math.random().toString(36).replace(/[^a-z0-9]+/g, '').substr(0, 10);
self.checkModuleLoaded();
if (deps.length) {
self.each(deps, self.parseFiles);
}
};
/**
* Loop trougth an array of element with the given function
* @param {Array|NodeList} array array to loop
* @param {Function} callback function to execute with each element
*/
Include.prototype.each = function (array, callback) {
var self = this,
i;
for (i = 0; i < array.length; i++) {
if (array[i] !== undefined && callback.call(self, array[i], i, array) === false) {
break;
}
}
}
/**
* Get element data id
* @param {String} name
* @param {Boolean} clean only clean the name
* @return {String}
*/
Include.prototype.getId = function (name, clean) {
return (clean ? '' : this.uid + '-') + name.replace(/[^a-z0-9]+/g, '');
}
/**
* Check if a module is loaded
*/
Include.prototype.checkModuleLoaded = function () {
var self = this;
self.each(waitingModules, function (module, i) {
var name = module[0],
dependencies = module[1],
exec = module[2],
args = [];
self.each(dependencies, function (dependencie, n, t) {
n = dependencie.push ? dependencie[0] : dependencie;
t = document.querySelector('[data-id*="' + self.getId(n, 1) + '"]');
if (t && t.nodeName == "LINK") {
args.push(null);
return;
}
if (modules[n] !== undefined) {
args.push(modules[n]);
}
});
if (dependencies.length === args.length || dependencies.length === 0) {
if (name === null && i+1 === waitingModules.length) {
waitingModules = [];
scriptCounter = 1;
}
exec = typeof exec == 'function' ? exec.apply(this, args) : exec;
modules[name] = exec;
}
});
}
/**
* onModuleLoaded
* @param {String} name name of the module
* @param {Number} index index of the module
*/
Include.prototype.onModuleLoaded = function (name, index) {
var self = this;
// Is this script add a waiting module ? If not, that's a "normal" script file
if (index > waitingModules.length) {
scriptCounter--;
modules[name] = modules[name] || scriptCounter;
} else if (waitingModules[0][0] === null) {
waitingModules[0][0] = name;
}
self.checkModuleLoaded();
}
/**
* On Load event
* @param {Event} event event of the load
* @param {Function} caller
*/
Include.prototype.onLoad = function (event, caller) {
var self = this,
target = (event.currentTarget || event.srcElement);
//Check if the script is realy loaded and executed
if (event.type !== "load" && target.readyState != "complete") {
return;
}
target.setAttribute('data-loaded', true);
self.onModuleLoaded(target.getAttribute('data-module'), target.getAttribute('data-count'));
// Old browser need to use the detachEvent method
if (target.attachEvent) {
target.detachEvent('onreadystatechange', caller);
} else {
target.removeEventListener('load', caller);
}
}
/**
* Watch for css load
* @param {Element} elem elem to check loading
*/
Include.prototype.watchCss = function (elem) {
var self = this,
sheets = document.styleSheets,
i = sheets.length,
href = elem.href.split('//').pop();
// loop on document stylesheets to check if media is loaded
while (i--) {
if (sheets[i].href.indexOf(href) != -1) {
elem.setAttribute('data-loaded', true);
self.onModuleLoaded(elem.getAttribute('data-module'), elem.getAttribute('data-count'));
return;
}
}
setTimeout(function () {
self.watchCss.call(self, elem);
});
}
/**
* Attach events to an element
* @param {Element} elem elem to attach event
* @param {Boolean} isJs is elem a script
*/
Include.prototype.attachEvents = function (elem, isJs) {
var self = this,
cb = function () {
var args = Array.prototype.slice.call(arguments);
args.push(cb);
self.onLoad.apply(self, args);
};
if (isJs) {
if (elem.attachEvent) {
elem.attachEvent('onreadystatechange', cb);
} else {
elem.addEventListener('load', cb, true);
}
} else {
self.watchCss(elem);
}
}
/**
* Check if a script already load
* @param {String} moduleName module to load
* @param {String} isJs type of file
*/
Include.prototype.checkExists = function (moduleName, isJs) {
var exists = false;
this.each(document.getElementsByTagName(isJs ? 'script' : 'link'), function (elem) {
if (elem.getAttribute('data-module') && elem.getAttribute('data-module') === moduleName) {
exists = elem;
return false;
}
});
return exists;
}
/**
* Create a script element to load asked module
* @param {String} moduleName name of the module
* @param {String} moduleFile file to include
* @param {String} isJs type of file
*/
Include.prototype.create = function (moduleName, moduleFile, isJs) {
var self = this;
//SetTimeout prevent the element create browser rush
setTimeout(function(){
var elem = self.checkExists.call(self, moduleName, isJs);
if (elem) {
return;
}
scriptCounter++;
elem = document.createElement(isJs ? 'script' : 'link');
if (isJs) {
elem.async = true;
elem.type = "text/javascript";
elem.src = moduleFile;
} else {
elem.media = "all";
elem.href = moduleFile;
elem.rel = "stylesheet"
}
elem.setAttribute('data-id', self.getId(moduleName));
elem.setAttribute('data-module', moduleName);
elem.setAttribute('data-count', scriptCounter);
elem.setAttribute('data-loaded', false);
if (baseElement) {
//prevent IE 6-8 bug (script executed before appenchild execution.
baseElement.parentNode.insertBefore(elem, baseElement);
} else {
head.appendChild(elem);
}
self.attachEvents.call(self, elem, isJs);
}, 0);
}
/**
* Parse a file to include
* @param {String} file file to parse
*/
Include.prototype.parseFiles = function (file) {
var moduleName = file.push ? file[0] : file,
moduleFile = file.push ? file[1] : file,
ext;
//Don't load module already loaded
if (modules[moduleName]) {
this.checkModuleLoaded();
return;
}
if (moduleFile.indexOf('//') == -1 && !/\.js/.test(moduleFile) && !/^http/.test(moduleFile)) {
moduleFile = moduleFile.replace(/\./g, '/');
moduleFile = moduleFile + '.js';
}
ext = moduleFile.split('.').pop() == 'js';
this.create.call(this, moduleName, moduleFile, ext);
}
/**
* @param {String} name the name of the module
* @param {Array} deps dependencies of the module
* @param {Function} module module definition
*/
environment['include'] = environment['require'] = environment['define'] = function (name, deps, module) {
return new Include(name, deps, module);
};
})(env);
export default env;

Wyświetl plik

@ -226,11 +226,27 @@
<!--<li>
<a href="#"><i class="fa fa-plane fa-fw"></i> Mission Planner</a>
</li> -->
{% load processingnode_extras %}
{% load processingnode_extras plugins %}
{% can_view_processing_nodes as view_nodes %}
{% can_add_processing_nodes as add_nodes %}
{% get_visible_processing_nodes as nodes %}
{% get_plugins_main_menus as plugin_menus %}
{% for menu in plugin_menus %}
<li>
<a href="{{menu.link}}"><i class="{{menu.css_icon}}"></i> {{menu.label}}{% if menu.has_submenu %}<span class="fa arrow"></span>{% endif %}</a>
{% if menu.has_submenu %}
<ul class="nav nav-second-level">
{% for menu in menu.submenu %}
<li>
<a href="{{menu.link}}"><i class="{{menu.css_icon}}"></i> {{menu.label}}{% if menu.has_submenu %}<span class="fa arrow"></span>{% endif %}</a>
</li>
{% endfor %}
</ul>
{% endif %}
</li>
{% endfor %}
{% if view_nodes %}
<li>

Wyświetl plik

@ -15,3 +15,8 @@ def get_plugins_css_includes():
# Flatten all urls for all plugins
css_urls = list(itertools.chain(*[plugin.get_include_css_urls() for plugin in get_active_plugins()]))
return "\n".join(map(lambda url: "<link href='{}' rel='stylesheet' type='text/css'>".format(url), css_urls))
@register.assignment_tag()
def get_plugins_main_menus():
# Flatten list of menus
return list(itertools.chain(*[plugin.main_menu() for plugin in get_active_plugins()]))

Wyświetl plik

@ -0,0 +1,22 @@
from django.test import Client
from rest_framework import status
from .classes import BootTestCase
class TestPlugins(BootTestCase):
def setUp(self):
pass
def tearDown(self):
pass
def test_core_plugins(self):
client = Client()
# We can access public files core plugins (without auth)
res = client.get('/plugins/measure/leaflet-measure.css')
self.assertEqual(res.status_code, status.HTTP_200_OK)
# TODO:
# test API endpoints
# test python hooks

Wyświetl plik

@ -6,7 +6,7 @@ from nodeodm.models import ProcessingNode
from .classes import BootTestCase
from .utils import start_processing_node
class TestWelcome(BootTestCase):
class TestWorker(BootTestCase):
def setUp(self):
super().setUp()

Wyświetl plik

@ -16,10 +16,10 @@ urlpatterns = [
url(r'^3d/project/(?P<project_pk>[^/.]+)/task/(?P<task_pk>[^/.]+)/$', app_views.model_display, name='model_display'),
url(r'^public/task/(?P<task_pk>[^/.]+)/map/$', public_views.map, name='public_map'),
url(r'^public/task/(?P<task_pk>[^/.]+)/iframe/map/$', public_views.map_iframe, name='public_map'),
url(r'^public/task/(?P<task_pk>[^/.]+)/3d/$', public_views.model_display, name='public_map'),
url(r'^public/task/(?P<task_pk>[^/.]+)/iframe/3d/$', public_views.model_display_iframe, name='public_map'),
url(r'^public/task/(?P<task_pk>[^/.]+)/json/$', public_views.task_json, name='public_map'),
url(r'^public/task/(?P<task_pk>[^/.]+)/iframe/map/$', public_views.map_iframe, name='public_iframe_map'),
url(r'^public/task/(?P<task_pk>[^/.]+)/3d/$', public_views.model_display, name='public_3d'),
url(r'^public/task/(?P<task_pk>[^/.]+)/iframe/3d/$', public_views.model_display_iframe, name='public_iframe_3d'),
url(r'^public/task/(?P<task_pk>[^/.]+)/json/$', public_views.task_json, name='public_json'),
url(r'^processingnode/([\d]+)/$', app_views.processing_node, name='processing_node'),

Wyświetl plik

@ -0,0 +1 @@
from .plugin import *

Wyświetl plik

@ -0,0 +1,13 @@
{
"name": "POSM GCP Interface",
"webodmMinVersion": "0.5.0",
"description": "A plugin to create GCP files from images",
"version": "0.1.0",
"author": "Piero Toffanin",
"email": "pt@masseranolabs.com",
"repository": "https://github.com/OpenDroneMap/WebODM",
"tags": ["gcp", "posm"],
"homepage": "https://github.com/OpenDroneMap/WebODM",
"experimental": true,
"deprecated": false
}

Wyświetl plik

@ -0,0 +1,19 @@
from app.plugins import PluginBase, Menu, MountPoint
from django.shortcuts import render
class Plugin(PluginBase):
def register(self):
pass
def main_menu(self):
return [Menu("GCP Editor", self.url("index.html"), "fa fa-map-marker fa-fw")]
def mount_points(self):
return [
MountPoint("/test", test)
]
def test(request):
return render(request, 'app/dashboard.html', {'title': 'PLUGIN!!'})

Wyświetl plik

@ -0,0 +1,35 @@
{
"main.css": "static/css/main.0b2dd337.css",
"main.css.map": "static/css/main.0b2dd337.css.map",
"main.js": "static/js/main.df429876.js",
"main.js.map": "static/js/main.df429876.js.map",
"static/media/add.png": "static/media/add.5a2714f3.png",
"static/media/add@2x.png": "static/media/add@2x.b53b9f2d.png",
"static/media/add_point.png": "static/media/add_point.e65f1d0c.png",
"static/media/add_point@2x.png": "static/media/add_point@2x.bf317640.png",
"static/media/add_point_green.png": "static/media/add_point_green.013c6b67.png",
"static/media/add_point_green@2x.png": "static/media/add_point_green@2x.1dd546dd.png",
"static/media/add_point_yellow.png": "static/media/add_point_yellow.a6d933c3.png",
"static/media/add_point_yellow@2x.png": "static/media/add_point_yellow@2x.5b290820.png",
"static/media/close.png": "static/media/close.729ab67b.png",
"static/media/close@2x.png": "static/media/close@2x.c65c9577.png",
"static/media/fit_markers.png": "static/media/fit_markers.be9754ad.png",
"static/media/fit_markers@2x.png": "static/media/fit_markers@2x.cf8c8fad.png",
"static/media/gcp-green.png": "static/media/gcp-green.cfc5c722.png",
"static/media/gcp-yellow.png": "static/media/gcp-yellow.3793065e.png",
"static/media/gcp.png": "static/media/gcp.44ed9ab1.png",
"static/media/layers-2x.png": "static/media/layers-2x.4f0283c6.png",
"static/media/layers.png": "static/media/layers.a6137456.png",
"static/media/loading.gif": "static/media/loading.e56d6770.gif",
"static/media/loading@2x.gif": "static/media/loading@2x.0ab4b1d1.gif",
"static/media/logo.png": "static/media/logo.b38a9426.png",
"static/media/marker-icon.png": "static/media/marker-icon.2273e3d8.png",
"static/media/point_icon.png": "static/media/point_icon.e206131a.png",
"static/media/point_icon@2x.png": "static/media/point_icon@2x.dd1da9a3.png",
"static/media/polygon_icon.png": "static/media/polygon_icon.83cffeed.png",
"static/media/polygon_icon@2x.png": "static/media/polygon_icon@2x.53277be6.png",
"static/media/providers.png": "static/media/providers.ad5af2f5.png",
"static/media/providers@2x.png": "static/media/providers@2x.51ed570c.png",
"static/media/search.png": "static/media/search.57a8b421.png",
"static/media/search@2x.png": "static/media/search@2x.44cf1bbe.png"
}

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.1 KiB

Wyświetl plik

@ -0,0 +1 @@
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="shortcut icon" href="/favicon.ico"><title>GCPi</title><link href="/static/css/main.0b2dd337.css" rel="stylesheet"></head><body><div id="root"></div><script type="text/javascript" src="/static/js/main.df429876.js"></script></body></html>

File diff suppressed because one or more lines are too long

Wyświetl plik

@ -0,0 +1 @@
{"version":3,"sources":[],"names":[],"mappings":"","file":"static/css/main.0b2dd337.css","sourceRoot":""}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 400 B

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 630 B

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 564 B

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1003 B

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 627 B

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.0 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 581 B

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 990 B

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 482 B

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 717 B

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 569 B

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 985 B

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.1 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 936 B

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 958 B

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.2 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 696 B

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 3.9 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 8.0 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 3.7 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.4 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 298 B

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 549 B

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 250 B

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 522 B

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 378 B

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 626 B

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 268 B

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 359 B

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 524 B

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 639 B