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