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):
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:
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'))
@ -149,7 +151,7 @@ class PluginAdmin(admin.ModelAdmin):
try:
disable_plugin(plugin_name)
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'))
@ -157,7 +159,7 @@ class PluginAdmin(admin.ModelAdmin):
try:
delete_plugin(plugin_name)
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'))
@ -201,15 +203,15 @@ class PluginAdmin(admin.ModelAdmin):
clear_plugins_cache()
init_plugins()
messages.info(request, "Plugin added successfully")
messages.info(request, _("Plugin added successfully"))
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):
os.remove(tmp_zip_path)
if os.path.exists(tmp_extract_path):
shutil.rmtree(tmp_extract_path)
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'))
@ -217,15 +219,16 @@ class PluginAdmin(admin.ModelAdmin):
def plugin_actions(self, obj):
plugin = get_plugin_by_name(obj.name, only_active=False)
return format_html(
'<a class="button" href="{}" {}>Disable</a>&nbsp;'
'<a class="button" href="{}" {}>Enable</a>'
'<a class="button" href="{}" {}>{}</a>&nbsp;'
'<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;')
,
reverse('admin:plugin-disable', args=[obj.pk]) if obj.enabled else '#',
'disabled' if not obj.enabled else '',
_('Disable'),
reverse('admin:plugin-enable', args=[obj.pk]) if not obj.enabled else '#',
'disabled' if obj.enabled else '',
_('Enable'),
reverse('admin:plugin-delete', args=[obj.pk]),
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.register()
Plugin.objects.get(pk=plugin_name).enable()
return p
def disable_plugin(plugin_name):
Plugin.objects.get(pk=plugin_name).disable()

Wyświetl plik

@ -165,6 +165,13 @@ class PluginBase(ABC):
"""
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):
"""
Should be overriden by plugins that want to add
@ -173,6 +180,19 @@ class PluginBase(ABC):
"""
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):
"""
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 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.views.static import serve
from urllib.parse import urlparse
@ -58,4 +58,12 @@ def api_view_handler(request, plugin_name=None):
if view:
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 Dashboard from './Dashboard';
import App from './App';
import SharePopup from './SharePopup';
import SystemJS from 'SystemJS';
if (!window.PluginsAPI){
@ -31,6 +32,7 @@ if (!window.PluginsAPI){
Map: factory.create(Map),
Dashboard: factory.create(Dashboard),
App: factory.create(App),
SharePopup: factory.create(SharePopup),
SystemJS,
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 ClipboardInput from './ClipboardInput';
import QRCode from 'qrcode.react';
import update from 'immutability-helper';
import $ from 'jquery';
import { _ } from '../classes/gettext';
@ -27,16 +28,32 @@ class SharePopup extends React.Component{
task: props.task,
togglingShare: false,
error: "",
showQR: false
showQR: false,
linkControls: [], // coming from plugins,
relShareLink: this.getRelShareLink()
};
this.handleEnableSharing = this.handleEnableSharing.bind(this);
}
getRelShareLink = () => {
return `/public/task/${this.props.task.id}/${this.props.linksTarget}/`;
}
componentDidMount(){
if (!this.state.task.public){
this.handleEnableSharing();
}
PluginsAPI.SharePopup.triggerAddLinkControl({
sharePopup: this
}, (ctrl) => {
if (!ctrl) return;
this.setState(update(this.state, {
linkControls: {$push: [ctrl]}
}));
});
}
handleEnableSharing(e){
@ -68,7 +85,7 @@ class SharePopup extends React.Component{
}
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 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>
</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" : "")}>
<label>
{_("HTML iframe:")}

Wyświetl plik

@ -2,7 +2,7 @@ from django.conf.urls import url, include
from django.views.i18n import JavaScriptCatalog
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 webodm import settings
@ -45,7 +45,7 @@ urlpatterns = [
# 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'^i18n/', include('django.conf.urls.i18n')),
]
] + root_url_patterns()
handler404 = app_views.handler404
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",
"version": "1.9.2",
"version": "1.9.3",
"description": "User-friendly, extendable application and API for processing aerial imagery.",
"main": "index.js",
"scripts": {