kopia lustrzana https://github.com/OpenDroneMap/WebODM
Short links plugin
rodzic
a061af7727
commit
f58e0b2acc
23
app/admin.py
23
app/admin.py
|
@ -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> '
|
||||
'<a class="button" href="{}" {}>Enable</a>'
|
||||
'<a class="button" href="{}" {}>{}</a> '
|
||||
'<a class="button" href="{}" {}>{}</a>'
|
||||
+ (' <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 ' ')
|
||||
,
|
||||
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
|
||||
)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
export default {
|
||||
namespace: "SharePopup",
|
||||
|
||||
endpoints: [
|
||||
"addLinkControl",
|
||||
]
|
||||
};
|
||||
|
|
@ -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:")}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
from .plugin import *
|
|
@ -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()
|
|
@ -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
|
||||
}
|
|
@ -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()),
|
||||
]
|
||||
|
|
@ -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>);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
.slcheckbox{
|
||||
font-weight: normal;
|
||||
font-size: 90%;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
PluginsAPI.SharePopup.addLinkControl([
|
||||
'shortlinks/build/SLCheckbox.js',
|
||||
'shortlinks/build/SLCheckbox.css'
|
||||
],function(args, SLCheckbox){
|
||||
return SLCheckbox;
|
||||
}
|
||||
);
|
|
@ -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": {
|
||||
|
|
Ładowanie…
Reference in New Issue