Edit short links plugin

pull/1028/head
Piero Toffanin 2021-07-28 14:48:16 -04:00
rodzic fa3411b270
commit 7183b4c0dc
8 zmienionych plików z 382 dodań i 0 usunięć

Wyświetl plik

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

Wyświetl plik

@ -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()

Wyświetl plik

@ -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
}

Wyświetl plik

@ -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())
]

Wyświetl plik

@ -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>);
}
}

Wyświetl plik

@ -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;
}
}
}

Wyświetl plik

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