kopia lustrzana https://github.com/OpenDroneMap/WebODM
Edit short links plugin
rodzic
fa3411b270
commit
7183b4c0dc
|
@ -0,0 +1 @@
|
||||||
|
from .plugin import *
|
|
@ -0,0 +1,120 @@
|
||||||
|
import math
|
||||||
|
import re
|
||||||
|
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from app.plugins.views import TaskView
|
||||||
|
from app.plugins import get_current_plugin, signals as plugin_signals
|
||||||
|
from django.dispatch import receiver
|
||||||
|
from app.plugins import GlobalDataStore
|
||||||
|
from django.http import Http404
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger('app.logger')
|
||||||
|
|
||||||
|
ds = GlobalDataStore('editshortlinks')
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
def getShortLinks(username):
|
||||||
|
return ds.get_json(username + "_shortlinks", {
|
||||||
|
't': {}, # task --> short id
|
||||||
|
'i': {} # short id --> task
|
||||||
|
})
|
||||||
|
|
||||||
|
class DeleteShortLink(TaskView):
|
||||||
|
def post(self, request, pk=None):
|
||||||
|
task = self.get_and_check_task(request, pk)
|
||||||
|
task_id = str(task.id)
|
||||||
|
username = str(request.user)
|
||||||
|
|
||||||
|
shortlinks = getShortLinks(username)
|
||||||
|
|
||||||
|
short_id = shortlinks['t'].get(task_id)
|
||||||
|
if short_id is not None:
|
||||||
|
del shortlinks['i'][short_id]
|
||||||
|
del shortlinks['t'][task_id]
|
||||||
|
|
||||||
|
ds.set_json(username + "_shortlinks", shortlinks)
|
||||||
|
|
||||||
|
return Response({'success': True}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
class EditShortLink(TaskView):
|
||||||
|
def post(self, request, pk=None):
|
||||||
|
task = self.get_and_check_task(request, pk)
|
||||||
|
task_id = str(task.id)
|
||||||
|
username = str(request.user)
|
||||||
|
short_id = request.data.get("shortId")
|
||||||
|
if not re.match(r'^[A-Za-z0-9_-]+$', short_id):
|
||||||
|
return Response({'error': _("Short URLs can only include letters, numbers, underscore and dash characters (A-Z, 0-9, _, -).")})
|
||||||
|
|
||||||
|
shortlinks = getShortLinks(username)
|
||||||
|
if shortlinks['i'].get(short_id, task_id) != task_id:
|
||||||
|
return Response({'error': _("This short URL is already taken.")})
|
||||||
|
|
||||||
|
# Replace previous if any
|
||||||
|
prev_short_id = shortlinks['t'].get(task_id)
|
||||||
|
shortlinks['t'][task_id] = short_id
|
||||||
|
|
||||||
|
if prev_short_id is not None:
|
||||||
|
del shortlinks['i'][prev_short_id]
|
||||||
|
|
||||||
|
shortlinks['i'][short_id] = task_id
|
||||||
|
|
||||||
|
ds.set_json(username + "_shortlinks", shortlinks)
|
||||||
|
|
||||||
|
return Response({'username': username, 'shortId': short_id}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
class GetShortLink(TaskView):
|
||||||
|
def post(self, request, pk=None):
|
||||||
|
task = self.get_and_check_task(request, pk)
|
||||||
|
task_id = str(task.id)
|
||||||
|
username = str(request.user)
|
||||||
|
shortlinks = getShortLinks(username)
|
||||||
|
|
||||||
|
if task_id in shortlinks['t']:
|
||||||
|
# Return existing short link
|
||||||
|
return Response({'username': username, 'shortId': shortlinks['t'][task_id]}, 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(username + "_counter", 0)
|
||||||
|
ds.set_int(username + "_counter", counter + 1)
|
||||||
|
|
||||||
|
short_id = gen_short_string(counter)
|
||||||
|
|
||||||
|
# task_id --> short id
|
||||||
|
shortlinks['t'][task_id] = short_id
|
||||||
|
|
||||||
|
# short id --> task_id
|
||||||
|
shortlinks['i'][short_id] = task_id
|
||||||
|
|
||||||
|
ds.set_json(username + "_shortlinks", shortlinks)
|
||||||
|
|
||||||
|
return Response({'username': username, 'shortId': short_id}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
def HandleShortLink(request, view_type, username, short_id):
|
||||||
|
shortlinks = getShortLinks(username)
|
||||||
|
if short_id in shortlinks['i']:
|
||||||
|
task_id = shortlinks['i'][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": "Editable Short Links",
|
||||||
|
"webodmMinVersion": "1.9.3",
|
||||||
|
"description": "Create editable short links when sharing task URLs",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": "Piero Toffanin",
|
||||||
|
"email": "pt@uav4geo.com",
|
||||||
|
"repository": "https://github.com/OpenDroneMap/WebODM",
|
||||||
|
"tags": ["editable", "short", "links"],
|
||||||
|
"homepage": "https://github.com/OpenDroneMap/WebODM",
|
||||||
|
"experimental": false,
|
||||||
|
"deprecated": false
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
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, EditShortLink, DeleteShortLink, HandleShortLink
|
||||||
|
|
||||||
|
class Plugin(PluginBase):
|
||||||
|
def build_jsx_components(self):
|
||||||
|
return ['SLControls.jsx']
|
||||||
|
|
||||||
|
def include_js_files(self):
|
||||||
|
return ['main.js']
|
||||||
|
|
||||||
|
def root_mount_points(self):
|
||||||
|
return [
|
||||||
|
MountPoint(r'^s(?P<view_type>[m3])/(?P<username>[^/.]+)/(?P<short_id>[A-Za-z0-9_-]+)/?$', HandleShortLink)
|
||||||
|
]
|
||||||
|
|
||||||
|
def api_mount_points(self):
|
||||||
|
return [
|
||||||
|
MountPoint('task/(?P<pk>[^/.]+)/shortlink', GetShortLink.as_view()),
|
||||||
|
MountPoint('task/(?P<pk>[^/.]+)/edit', EditShortLink.as_view()),
|
||||||
|
MountPoint('task/(?P<pk>[^/.]+)/delete', DeleteShortLink.as_view())
|
||||||
|
]
|
||||||
|
|
|
@ -0,0 +1,200 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import './SLControls.scss';
|
||||||
|
import ErrorMessage from 'webodm/components/ErrorMessage';
|
||||||
|
import { _, interpolate } from 'webodm/classes/gettext';
|
||||||
|
import $ from 'jquery';
|
||||||
|
|
||||||
|
export default class SLControls extends React.Component{
|
||||||
|
static defaultProps = {
|
||||||
|
sharePopup: null
|
||||||
|
};
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
sharePopup: PropTypes.object.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props){
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
error: '',
|
||||||
|
loading: false,
|
||||||
|
useShortLink: false,
|
||||||
|
shortId: '',
|
||||||
|
editingShortId: false,
|
||||||
|
showEditSuccess: false,
|
||||||
|
savingShortId: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRelShareLink = (res, onSuccess = () => {}) => {
|
||||||
|
if (res.shortId){
|
||||||
|
const linksTarget = this.props.sharePopup.props.linksTarget;
|
||||||
|
|
||||||
|
const {username, shortId} = res;
|
||||||
|
const linksTargetChar = linksTarget === '3d' ? '3' : 'm';
|
||||||
|
|
||||||
|
const relShareLink = `s${linksTargetChar}/${username}/${shortId}`;
|
||||||
|
this.props.sharePopup.setState({relShareLink});
|
||||||
|
this.setState({shortId});
|
||||||
|
onSuccess();
|
||||||
|
}else if (res.error){
|
||||||
|
this.setState({error: res.error});
|
||||||
|
}else this.setState({error: interpolate(_('Invalid response from server: %(error)s'), { error: JSON.stringify(res)})});
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleShortLinks = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (!this.state.useShortLink && !this.state.loading){
|
||||||
|
this.setState({loading: true});
|
||||||
|
|
||||||
|
const task = this.props.sharePopup.props.task;
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
type: 'POST',
|
||||||
|
url: `/api/plugins/editshortlinks/task/${task.id}/shortlink`,
|
||||||
|
contentType: 'application/json'
|
||||||
|
}).done(res => {
|
||||||
|
this.updateRelShareLink(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});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleEdit = () => {
|
||||||
|
this.setState({editingShortId: true});
|
||||||
|
setTimeout(() => {
|
||||||
|
this.editTextbox.focus();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
handleSave = () => {
|
||||||
|
if (!this.state.savingShortId){
|
||||||
|
this.setState({savingShortId: true});
|
||||||
|
|
||||||
|
const task = this.props.sharePopup.props.task;
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
type: 'POST',
|
||||||
|
url: `/api/plugins/editshortlinks/task/${task.id}/edit`,
|
||||||
|
contentType: 'application/json',
|
||||||
|
data: JSON.stringify({
|
||||||
|
shortId: this.state.shortId
|
||||||
|
}),
|
||||||
|
dataType: 'json',
|
||||||
|
}).done(res => {
|
||||||
|
this.updateRelShareLink(res, () => {
|
||||||
|
this.setState({editingShortId: false, showEditSuccess: true});
|
||||||
|
setTimeout(() => {
|
||||||
|
this.setState({showEditSuccess: false});
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
}).fail(error => {
|
||||||
|
this.setState({error: interpolate(_('Invalid response from server: %(error)s'), { error }), loading: false});
|
||||||
|
}).always(() => {
|
||||||
|
this.setState({savingShortId: false});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleEditShortId = e => {
|
||||||
|
this.setState({shortId: e.target.value});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDelete = e => {
|
||||||
|
if (window.confirm(_("Are you sure?"))){
|
||||||
|
this.setState({loading: true});
|
||||||
|
const task = this.props.sharePopup.props.task;
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
type: 'POST',
|
||||||
|
url: `/api/plugins/editshortlinks/task/${task.id}/delete`,
|
||||||
|
contentType: 'application/json',
|
||||||
|
dataType: 'json',
|
||||||
|
}).done(res => {
|
||||||
|
if (res.success){
|
||||||
|
this.setState({useShortLink: false, shortId: ""});
|
||||||
|
this.props.sharePopup.setState({relShareLink: this.props.sharePopup.getRelShareLink()});
|
||||||
|
}else if (res.error){
|
||||||
|
this.setState({error: res.error});
|
||||||
|
}else{
|
||||||
|
this.setState({error: interpolate(_('Invalid response from server: %(error)s'), { error: JSON.stringify(res) })});
|
||||||
|
}
|
||||||
|
}).fail(error => {
|
||||||
|
this.setState({error: interpolate(_('Invalid response from server: %(error)s'), { error }), loading: false});
|
||||||
|
}).always(() => {
|
||||||
|
this.setState({loading: false});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleShortIdKeyPress = e => {
|
||||||
|
if (e.key === 'Enter'){
|
||||||
|
this.handleSave();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render(){
|
||||||
|
const { loading, useShortLink, savingShortId,
|
||||||
|
shortId, editingShortId, showEditSuccess } = this.state;
|
||||||
|
|
||||||
|
const controls = [<label className="slcheckbox" >
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
disabled={loading}
|
||||||
|
checked={useShortLink}
|
||||||
|
onChange={this.toggleShortLinks}
|
||||||
|
/> {_("Short Link")}
|
||||||
|
</label>];
|
||||||
|
|
||||||
|
if (useShortLink){
|
||||||
|
let editIcon = "fa-edit";
|
||||||
|
if (showEditSuccess) editIcon = "fa-check";
|
||||||
|
|
||||||
|
const editButton = (<button className="btn btn-secondary" type="button"
|
||||||
|
title={_("Edit")}
|
||||||
|
onClick={this.handleEdit}><i className={"fa " + editIcon}></i></button>);
|
||||||
|
|
||||||
|
let saveIcon = "fa-save";
|
||||||
|
if (savingShortId) saveIcon = "fa-circle-notch fa-spin";
|
||||||
|
|
||||||
|
const saveButton = (<button className="btn btn-secondary" type="button"
|
||||||
|
disabled={shortId.length === 0 || savingShortId}
|
||||||
|
title={_("Save")} onClick={this.handleSave}><i className={"fa " + saveIcon}></i></button>);
|
||||||
|
|
||||||
|
controls.push(<div className="input-group">
|
||||||
|
<input
|
||||||
|
className="form-control"
|
||||||
|
readOnly={!editingShortId}
|
||||||
|
value={shortId}
|
||||||
|
type="text"
|
||||||
|
placeholder={_("Suffix")}
|
||||||
|
disabled={loading}
|
||||||
|
onChange={this.handleEditShortId}
|
||||||
|
onKeyPress={this.handleShortIdKeyPress}
|
||||||
|
ref={(ref) => { this.editTextbox = ref; }}
|
||||||
|
></input>
|
||||||
|
|
||||||
|
<span className="input-group-btn">
|
||||||
|
{editingShortId ? saveButton : editButton}
|
||||||
|
</span>
|
||||||
|
</div>);
|
||||||
|
|
||||||
|
controls.push(<div>
|
||||||
|
<a className="sldelete" onClick={this.handleDelete}><i className="fa fa-trash"></i> {_("Delete Short Link")}</a>
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (<div className="slcontrols">
|
||||||
|
{controls}
|
||||||
|
<ErrorMessage bind={[this, "error"]} />
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
.slcontrols{
|
||||||
|
.slcheckbox{
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 90%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.sldelete{
|
||||||
|
user-select: none;
|
||||||
|
margin-top: 8px;
|
||||||
|
display: block;
|
||||||
|
font-size: 80%;
|
||||||
|
&:hover{
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
PluginsAPI.SharePopup.addLinkControl([
|
||||||
|
'editshortlinks/build/SLControls.js',
|
||||||
|
'editshortlinks/build/SLControls.css'
|
||||||
|
],function(args, SLControls){
|
||||||
|
return SLControls;
|
||||||
|
}
|
||||||
|
);
|
Ładowanie…
Reference in New Issue