Short links plugin

pull/1009/head
Piero Toffanin 2021-07-01 15:30:45 -04:00
rodzic a061af7727
commit f58e0b2acc
17 zmienionych plików z 267 dodań i 17 usunięć

Wyświetl plik

@ -139,9 +139,11 @@ class PluginAdmin(admin.ModelAdmin):
def plugin_enable(self, request, plugin_name, *args, **kwargs): def plugin_enable(self, request, plugin_name, *args, **kwargs):
try: try:
enable_plugin(plugin_name) p = enable_plugin(plugin_name)
if p.requires_restart():
messages.warning(request, _("Restart required. Please restart WebODM to enable %(plugin)s") % {'plugin': plugin_name})
except Exception as e: except Exception as e:
messages.warning(request, "Cannot enable plugin {}: {}".format(plugin_name, str(e))) messages.warning(request, _("Cannot enable plugin %(plugin)s: %(message)s") % {'plugin': plugin_name, 'message': str(e)})
return HttpResponseRedirect(reverse('admin:app_plugin_changelist')) return HttpResponseRedirect(reverse('admin:app_plugin_changelist'))
@ -149,7 +151,7 @@ class PluginAdmin(admin.ModelAdmin):
try: try:
disable_plugin(plugin_name) disable_plugin(plugin_name)
except Exception as e: except Exception as e:
messages.warning(request, "Cannot disable plugin {}: {}".format(plugin_name, str(e))) messages.warning(request, _("Cannot disable plugin %(plugin)s: %(message)s") % {'plugin': plugin_name, 'message': str(e)})
return HttpResponseRedirect(reverse('admin:app_plugin_changelist')) return HttpResponseRedirect(reverse('admin:app_plugin_changelist'))
@ -157,7 +159,7 @@ class PluginAdmin(admin.ModelAdmin):
try: try:
delete_plugin(plugin_name) delete_plugin(plugin_name)
except Exception as e: except Exception as e:
messages.warning(request, "Cannot delete plugin {}: {}".format(plugin_name, str(e))) messages.warning(request, _("Cannot delete plugin %(plugin)s: %(message)s") % {'plugin': plugin_name, 'message': str(e)})
return HttpResponseRedirect(reverse('admin:app_plugin_changelist')) return HttpResponseRedirect(reverse('admin:app_plugin_changelist'))
@ -201,15 +203,15 @@ class PluginAdmin(admin.ModelAdmin):
clear_plugins_cache() clear_plugins_cache()
init_plugins() init_plugins()
messages.info(request, "Plugin added successfully") messages.info(request, _("Plugin added successfully"))
except Exception as e: except Exception as e:
messages.warning(request, "Cannot load plugin: {}".format(str(e))) messages.warning(request, _("Cannot load plugin: %(message)s") % {'message': str(e)})
if os.path.exists(tmp_zip_path): if os.path.exists(tmp_zip_path):
os.remove(tmp_zip_path) os.remove(tmp_zip_path)
if os.path.exists(tmp_extract_path): if os.path.exists(tmp_extract_path):
shutil.rmtree(tmp_extract_path) shutil.rmtree(tmp_extract_path)
else: else:
messages.error(request, "You need to upload a zip file") messages.error(request, _("You need to upload a zip file"))
return HttpResponseRedirect(reverse('admin:app_plugin_changelist')) return HttpResponseRedirect(reverse('admin:app_plugin_changelist'))
@ -217,15 +219,16 @@ class PluginAdmin(admin.ModelAdmin):
def plugin_actions(self, obj): def plugin_actions(self, obj):
plugin = get_plugin_by_name(obj.name, only_active=False) plugin = get_plugin_by_name(obj.name, only_active=False)
return format_html( return format_html(
'<a class="button" href="{}" {}>Disable</a>&nbsp;' '<a class="button" href="{}" {}>{}</a>&nbsp;'
'<a class="button" href="{}" {}>Enable</a>' '<a class="button" href="{}" {}>{}</a>'
+ ('&nbsp;<a class="button" href="{}" onclick="return confirm(\'Are you sure you want to delete {}?\')"><i class="fa fa-trash"></i></a>' if not plugin.is_persistent() else '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;') + ('&nbsp;<a class="button" href="{}" onclick="return confirm(\'Are you sure you want to delete {}?\')"><i class="fa fa-trash"></i></a>' if not plugin.is_persistent() else '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;')
, ,
reverse('admin:plugin-disable', args=[obj.pk]) if obj.enabled else '#', reverse('admin:plugin-disable', args=[obj.pk]) if obj.enabled else '#',
'disabled' if not obj.enabled else '', 'disabled' if not obj.enabled else '',
_('Disable'),
reverse('admin:plugin-enable', args=[obj.pk]) if not obj.enabled else '#', reverse('admin:plugin-enable', args=[obj.pk]) if not obj.enabled else '#',
'disabled' if obj.enabled else '', 'disabled' if obj.enabled else '',
_('Enable'),
reverse('admin:plugin-delete', args=[obj.pk]), reverse('admin:plugin-delete', args=[obj.pk]),
obj.name obj.name
) )

Wyświetl plik

@ -325,6 +325,7 @@ def enable_plugin(plugin_name):
p = get_plugin_by_name(plugin_name, only_active=False) p = get_plugin_by_name(plugin_name, only_active=False)
p.register() p.register()
Plugin.objects.get(pk=plugin_name).enable() Plugin.objects.get(pk=plugin_name).enable()
return p
def disable_plugin(plugin_name): def disable_plugin(plugin_name):
Plugin.objects.get(pk=plugin_name).disable() Plugin.objects.get(pk=plugin_name).disable()

Wyświetl plik

@ -165,6 +165,13 @@ class PluginBase(ABC):
""" """
return [] return []
def requires_restart(self):
"""
Whether the plugin requires an app restart to
function properly
"""
return len(self.root_mount_points()) > 0
def main_menu(self): def main_menu(self):
""" """
Should be overriden by plugins that want to add Should be overriden by plugins that want to add
@ -173,6 +180,19 @@ class PluginBase(ABC):
""" """
return [] return []
def root_mount_points(self):
"""
Should be overriden by plugins that want to
add routes to the root view controller.
CAUTION: this should be used sparingly, as
routes could conflict with other plugins and
future versions of WebODM might break the routes.
It's recommended to use app_mount_points, unless
you know what you are doing.
:return: [] of MountPoint objects
"""
return []
def app_mount_points(self): def app_mount_points(self):
""" """
Should be overriden by plugins that want to connect Should be overriden by plugins that want to connect

Wyświetl plik

@ -5,7 +5,7 @@ from app.api.workers import CheckTask as CheckTask
from app.api.workers import GetTaskResult as GetTaskResult from app.api.workers import GetTaskResult as GetTaskResult
from django.http import HttpResponse, Http404 from django.http import HttpResponse, Http404
from .functions import get_plugin_by_name from .functions import get_plugin_by_name, get_active_plugins
from django.conf.urls import url from django.conf.urls import url
from django.views.static import serve from django.views.static import serve
from urllib.parse import urlparse from urllib.parse import urlparse
@ -58,4 +58,12 @@ def api_view_handler(request, plugin_name=None):
if view: if view:
return view(request, *args, **kwargs) return view(request, *args, **kwargs)
raise Http404("No valid routes") raise Http404("No valid routes")
def root_url_patterns():
result = []
for p in get_active_plugins():
for mount_point in p.root_mount_points():
result.append(url(mount_point.url, mount_point.view, *mount_point.args, **mount_point.kwargs))
return result

Wyświetl plik

@ -3,6 +3,7 @@ import ApiFactory from './ApiFactory';
import Map from './Map'; import Map from './Map';
import Dashboard from './Dashboard'; import Dashboard from './Dashboard';
import App from './App'; import App from './App';
import SharePopup from './SharePopup';
import SystemJS from 'SystemJS'; import SystemJS from 'SystemJS';
if (!window.PluginsAPI){ if (!window.PluginsAPI){
@ -31,6 +32,7 @@ if (!window.PluginsAPI){
Map: factory.create(Map), Map: factory.create(Map),
Dashboard: factory.create(Dashboard), Dashboard: factory.create(Dashboard),
App: factory.create(App), App: factory.create(App),
SharePopup: factory.create(SharePopup),
SystemJS, SystemJS,
events events

Wyświetl plik

@ -0,0 +1,8 @@
export default {
namespace: "SharePopup",
endpoints: [
"addLinkControl",
]
};

Wyświetl plik

@ -5,6 +5,7 @@ import ErrorMessage from './ErrorMessage';
import Utils from '../classes/Utils'; import Utils from '../classes/Utils';
import ClipboardInput from './ClipboardInput'; import ClipboardInput from './ClipboardInput';
import QRCode from 'qrcode.react'; import QRCode from 'qrcode.react';
import update from 'immutability-helper';
import $ from 'jquery'; import $ from 'jquery';
import { _ } from '../classes/gettext'; import { _ } from '../classes/gettext';
@ -27,16 +28,32 @@ class SharePopup extends React.Component{
task: props.task, task: props.task,
togglingShare: false, togglingShare: false,
error: "", error: "",
showQR: false showQR: false,
linkControls: [], // coming from plugins,
relShareLink: this.getRelShareLink()
}; };
this.handleEnableSharing = this.handleEnableSharing.bind(this); this.handleEnableSharing = this.handleEnableSharing.bind(this);
} }
getRelShareLink = () => {
return `/public/task/${this.props.task.id}/${this.props.linksTarget}/`;
}
componentDidMount(){ componentDidMount(){
if (!this.state.task.public){ if (!this.state.task.public){
this.handleEnableSharing(); this.handleEnableSharing();
} }
PluginsAPI.SharePopup.triggerAddLinkControl({
sharePopup: this
}, (ctrl) => {
if (!ctrl) return;
this.setState(update(this.state, {
linkControls: {$push: [ctrl]}
}));
});
} }
handleEnableSharing(e){ handleEnableSharing(e){
@ -68,7 +85,7 @@ class SharePopup extends React.Component{
} }
render(){ render(){
const shareLink = Utils.absoluteUrl(`/public/task/${this.state.task.id}/${this.props.linksTarget}/`); const shareLink = Utils.absoluteUrl(this.state.relShareLink);
const iframeUrl = Utils.absoluteUrl(`public/task/${this.state.task.id}/iframe/${this.props.linksTarget}/`); const iframeUrl = Utils.absoluteUrl(`public/task/${this.state.task.id}/iframe/${this.props.linksTarget}/`);
const iframeCode = `<iframe scrolling="no" title="WebODM" width="61.8033%" height="360" frameBorder="0" src="${iframeUrl}"></iframe>`; const iframeCode = `<iframe scrolling="no" title="WebODM" width="61.8033%" height="360" frameBorder="0" src="${iframeUrl}"></iframe>`;
@ -112,6 +129,12 @@ class SharePopup extends React.Component{
/> />
</label> </label>
</div> </div>
<div className={"form-group " + (this.state.showQR || this.state.linkControls.length === 0 ? "hide" : "")}>
{this.state.linkControls.map((Ctrl, i) =>
<Ctrl key={i}
sharePopup={this}
/>)}
</div>
<div className={"form-group " + (this.state.showQR ? "hide" : "")}> <div className={"form-group " + (this.state.showQR ? "hide" : "")}>
<label> <label>
{_("HTML iframe:")} {_("HTML iframe:")}

Wyświetl plik

@ -2,7 +2,7 @@ from django.conf.urls import url, include
from django.views.i18n import JavaScriptCatalog from django.views.i18n import JavaScriptCatalog
from .views import app as app_views, public as public_views, dev as dev_views from .views import app as app_views, public as public_views, dev as dev_views
from .plugins.views import app_view_handler from .plugins.views import app_view_handler, root_url_patterns
from app.boot import boot from app.boot import boot
from webodm import settings from webodm import settings
@ -45,7 +45,7 @@ urlpatterns = [
# TODO: add caching: https://docs.djangoproject.com/en/3.1/topics/i18n/translation/#note-on-performance # TODO: add caching: https://docs.djangoproject.com/en/3.1/topics/i18n/translation/#note-on-performance
url(r'^jsi18n/', JavaScriptCatalog.as_view(packages=['app']), name='javascript-catalog'), url(r'^jsi18n/', JavaScriptCatalog.as_view(packages=['app']), name='javascript-catalog'),
url(r'^i18n/', include('django.conf.urls.i18n')), url(r'^i18n/', include('django.conf.urls.i18n')),
] ] + root_url_patterns()
handler404 = app_views.handler404 handler404 = app_views.handler404
handler500 = app_views.handler500 handler500 = app_views.handler500

Wyświetl plik

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

Wyświetl plik

@ -0,0 +1,58 @@
import math
from rest_framework import status
from rest_framework.response import Response
from app.plugins.views import TaskView
from app.plugins import get_current_plugin
from app.plugins import GlobalDataStore
from django.http import Http404
from django.shortcuts import redirect
ds = GlobalDataStore('shortlinks')
def gen_short_string(num):
num = int(abs(num))
def nbase(num, numerals="abcdefghijklmnopqrstuvwxyz0123456789"):
return ((num == 0) and numerals[0]) or (nbase(num // len(numerals), numerals).lstrip(numerals[0]) + numerals[num % len(numerals)])
return nbase(num)
class GetShortLink(TaskView):
def post(self, request, pk=None):
task = self.get_and_check_task(request, pk)
key = str(task.id)
if ds.has_key(key):
# Return existing short link
return Response({'shortId': ds.get_string(key)}, status=status.HTTP_200_OK)
else:
# Compute short link, store it
# Not atomic, but this shouldn't be a big problem
counter = ds.get_int("counter", 0)
ds.set_int("counter", counter + 1)
short_id = gen_short_string(counter)
# TaskId --> short id
ds.set_string(key, short_id)
# short id --> taskId
ds.set_string(short_id, str(task.id))
return Response({'shortId': short_id}, status=status.HTTP_200_OK)
def HandleShortLink(request, view_type, short_id):
if ds.has_key(short_id):
task_id = ds.get_string(short_id)
if view_type == 'm':
v = 'map'
elif view_type == '3':
v = '3d'
return redirect('/public/task/{}/{}/'.format(task_id, v))
else:
raise Http404()

Wyświetl plik

@ -0,0 +1,13 @@
{
"name": "Short Links",
"webodmMinVersion": "1.9.3",
"description": "Create short links when sharing task URLs",
"version": "1.0.0",
"author": "Piero Toffanin",
"email": "pt@uav4geo.com",
"repository": "https://github.com/OpenDroneMap/WebODM",
"tags": ["short", "links"],
"homepage": "https://github.com/OpenDroneMap/WebODM",
"experimental": false,
"deprecated": false
}

Wyświetl plik

@ -0,0 +1,23 @@
from app.plugins import PluginBase, Menu, MountPoint
from django.shortcuts import render
from django.contrib.auth.decorators import login_required
from django.utils.translation import gettext as _
from .api import GetShortLink, HandleShortLink
class Plugin(PluginBase):
def build_jsx_components(self):
return ['SLCheckbox.jsx']
def include_js_files(self):
return ['main.js']
def root_mount_points(self):
return [
MountPoint(r'^s(?P<view_type>[m3])(?P<short_id>[a-z0-9]+)/?$', HandleShortLink)
]
def api_mount_points(self):
return [
MountPoint('task/(?P<pk>[^/.]+)/shortlink', GetShortLink.as_view()),
]

Wyświetl plik

@ -0,0 +1,79 @@
import React from 'react';
import PropTypes from 'prop-types';
import './SLCheckbox.scss';
import ErrorMessage from 'webodm/components/ErrorMessage';
import { _, interpolate } from 'webodm/classes/gettext';
import $ from 'jquery';
export default class SLCheckbox extends React.Component{
static defaultProps = {
sharePopup: null
};
static propTypes = {
sharePopup: PropTypes.object.isRequired
};
constructor(props){
super(props);
this.state = {
error: '',
loading: false,
useShortLink: false,
};
}
componentDidMount(){
}
toggleShortLinks = (e) => {
e.stopPropagation();
if (!this.state.useShortLink && !this.state.loading){
this.setState({loading: true});
const task = this.props.sharePopup.props.task;
const linksTarget = this.props.sharePopup.props.linksTarget;
$.ajax({
type: 'POST',
url: `/api/plugins/shortlinks/task/${task.id}/shortlink`,
contentType: 'application/json'
}).done(res => {
const shortId = res.shortId;
const linksTargetChar = linksTarget === '3d' ? '3' : 'm';
const relShareLink = `s${linksTargetChar}${shortId}`;
if (shortId) this.props.sharePopup.setState({relShareLink});
else this.setState({error: interpolate(_('Invalid response from server: %(error)s'), { error: JSON.stringify(res)})});
this.setState({loading: false, useShortLink: !this.state.useShortLink});
}).fail(error => {
this.setState({error: interpolate(_('Invalid response from server: %(error)s'), { error }), loading: false});
});
}else{
this.props.sharePopup.setState({relShareLink: this.props.sharePopup.getRelShareLink()});
this.setState({useShortLink: !this.state.useShortLink});
}
}
render(){
const { error, loading, useShortLink } = this.state;
if (error) return (<ErrorMessage bind={[this, "error"]} />);
return (<label className="slcheckbox">
{loading ?
<i className="fa fa-sync fa-spin fa-fw"></i>
: ""}
<input
className={this.props.sharePopup.state.togglingShare ? "hide" : ""}
type="checkbox"
checked={useShortLink}
onChange={this.toggleShortLinks}
/> {_("Use Short Link")}
</label>);
}
}

Wyświetl plik

@ -0,0 +1,4 @@
.slcheckbox{
font-weight: normal;
font-size: 90%;
}

Wyświetl plik

@ -0,0 +1,7 @@
PluginsAPI.SharePopup.addLinkControl([
'shortlinks/build/SLCheckbox.js',
'shortlinks/build/SLCheckbox.css'
],function(args, SLCheckbox){
return SLCheckbox;
}
);

Wyświetl plik

@ -1,6 +1,6 @@
{ {
"name": "WebODM", "name": "WebODM",
"version": "1.9.2", "version": "1.9.3",
"description": "User-friendly, extendable application and API for processing aerial imagery.", "description": "User-friendly, extendable application and API for processing aerial imagery.",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {