Working on skeleton

pull/1122/head
Luca Di Leo 2022-01-04 09:13:15 -08:00
rodzic 500420941d
commit a9a474ad46
22 zmienionych plików z 881 dodań i 0 usunięć

Wyświetl plik

@ -0,0 +1,7 @@
<h1 align="center">DroneDB</h1>
Welcome to DroneDB Plugin!
DroneDB is a WebODM add-on that enables you to import and export your files to DroneDB.

Wyświetl plik

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

Wyświetl plik

@ -0,0 +1,137 @@
import importlib
import requests
import os
from os import path
from app import models, pending_actions
from app.plugins.views import TaskView
from app.plugins.worker import run_function_async
from app.plugins import get_current_plugin
from worker.celery import app
from rest_framework.response import Response
from rest_framework import status
#from .platform_helper import get_all_platforms, get_platform_by_name
class ImportFolderTaskView(TaskView):
def post(self, request, project_pk=None, pk=None):
task = self.get_and_check_task(request, pk)
# Read form data
folder_url = request.data.get('selectedFolderUrl', None)
platform_name = request.data.get('platform', None)
# Make sure both values are set
if folder_url == None or platform_name == None:
return Response({'error': 'Folder URL and platform name must be set.'}, status=status.HTTP_400_BAD_REQUEST)
# Fetch the platform by name
platform = get_platform_by_name(platform_name)
# Make sure that the platform actually exists
if platform == None:
return Response({'error': 'Failed to find a platform with the name \'{}\''.format(platform_name)}, status=status.HTTP_400_BAD_REQUEST)
# Verify that the folder url is valid
if platform.verify_folder_url(folder_url) == None:
return Response({'error': 'Invalid URL'}, status=status.HTTP_400_BAD_REQUEST)
# Get the files from the folder
files = platform.import_from_folder(folder_url)
# Update the task with the new information
task.console_output += "Importing {} images...\n".format(len(files))
task.images_count = len(files)
task.pending_action = pending_actions.IMPORT
task.save()
# Associate the folder url with the project and task
combined_id = "{}_{}".format(project_pk, pk)
get_current_plugin().get_global_data_store().set_string(combined_id, folder_url)
# Start importing the files in the background
serialized = [file.serialize() for file in files]
run_function_async(import_files, task.id, serialized)
return Response({}, status=status.HTTP_200_OK)
class CheckUrlTaskView(TaskView):
def get(self, request, project_pk=None, pk=None):
# Assert that task exists
self.get_and_check_task(request, pk)
# Check if there is an imported url associated with the project and task
combined_id = "{}_{}".format(project_pk, pk)
folder_url = get_current_plugin().get_global_data_store().get_string(combined_id, default = None)
if folder_url == None:
return Response({}, status=status.HTTP_200_OK)
else:
return Response({'folder_url': folder_url}, status=status.HTTP_200_OK)
class PlatformsVerifyTaskView(TaskView):
def get(self, request, platform_name):
# Read the form data
folder_url = request.GET.get('folderUrl', None)
# Fetch the platform by name
platform = get_platform_by_name(platform_name)
# Make sure that the platform actually exists
if platform == None:
return Response({'error': 'Failed to find a platform with the name \'{}\''.format(platform_name)}, status=status.HTTP_400_BAD_REQUEST)
# Verify that the folder url is valid
folder = platform.verify_folder_url(folder_url)
if folder == None:
return Response({'error': 'Invalid URL'}, status=status.HTTP_400_BAD_REQUEST)
# Return the folder
return Response({'folder': folder.serialize()}, status=status.HTTP_200_OK)
class PlatformsTaskView(TaskView):
def get(self, request):
# Fetch and return all platforms
platforms = get_all_platforms()
return Response({'platforms': [platform.serialize(user = request.user) for platform in platforms]}, status=status.HTTP_200_OK)
def import_files(task_id, files):
import requests
from app import models
from app.plugins import logger
def download_file(task, file):
path = task.task_path(file['name'])
download_stream = requests.get(file['url'], stream=True, timeout=60)
with open(path, 'wb') as fd:
for chunk in download_stream.iter_content(4096):
fd.write(chunk)
models.ImageUpload.objects.create(task=task, image=path)
logger.info("Will import {} files".format(len(files)))
task = models.Task.objects.get(pk=task_id)
task.create_task_directories()
task.save()
try:
downloaded_total = 0
for file in files:
download_file(task, file)
task.check_if_canceled()
models.Task.objects.filter(pk=task.id).update(upload_progress=(float(downloaded_total) / float(len(files))))
downloaded_total += 1
except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e:
raise NodeServerError(e)
task.refresh_from_db()
task.pending_action = None
task.processing_time = 0
task.partial = False
task.save()

Wyświetl plik

@ -0,0 +1,66 @@
import requests
from django import forms
from django.contrib import messages
from django.http import HttpResponse
from django.shortcuts import render
from django.contrib.auth.decorators import login_required
from app.plugins import logger
#from .platform_helper import get_all_extended_platforms
""" class DynamicForm(forms.Form):
"""This dynamic form will go through all the extended platforms, and retrieve their fields"""
def __init__(self, *args, **kwargs):
ds = kwargs.pop('data_store')
super(DynamicForm, self).__init__(*args, **kwargs)
extended_platforms = get_all_extended_platforms()
for platform in extended_platforms:
for form_field in platform.get_form_fields():
django_field = form_field.get_django_field(ds)
django_field.group = platform.name
self.fields[form_field.field_id] = django_field
"""
def HomeView(plugin):
@login_required
def view(request):
""" ds = plugin.get_user_data_store(request.user)
# if this is a POST request we need to process the form data
if request.method == "POST":
form = DynamicForm(request.POST, data_store = ds)
if form.is_valid():
extended_platforms = get_all_extended_platforms()
for platform in extended_platforms:
for form_field in platform.get_form_fields():
form_field.save_value(ds, form)
messages.success(request, "Configuration updated successfuly!")
else:
form = DynamicForm(data_store = ds) """
return render(
request,
plugin.template_path("app.html"),
{"title": "DroneDB"},
)
return view
def LoadButtonsView(plugin):
def view(request):
return render(
request,
plugin.template_path("load_buttons.js"),
{
"api_url": "/api" + plugin.public_url("").rstrip("/"),
},
content_type="text/javascript",
)
return view

Wyświetl plik

@ -0,0 +1,40 @@
from app.plugins import PluginBase, Menu, MountPoint, logger
from .api_views import PlatformsTaskView, PlatformsVerifyTaskView, ImportFolderTaskView, CheckUrlTaskView
from .app_views import HomeView, LoadButtonsView
#from .platform_helper import get_all_extended_platforms
class Plugin(PluginBase):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def main_menu(self):
return [Menu("DroneDB", self.public_url(""), "fa-cloud-download-alt fa fa-fw")]
def include_js_files(self):
return ["load_buttons.js"]
def include_css_files(self):
return ["build/ImportView.css", "build/TaskView.css"]
def build_jsx_components(self):
return ["ImportView.jsx", "TaskView.jsx"]
""" def api_mount_points(self):
#api_views = [api_view for platform in get_all_extended_platforms() for api_view in platform.get_api_views()]
# mount_points = [MountPoint(path, view) for (path, view) in api_views]
# Add mount points for each extended platform that might require us to do so
return mount_points + [
MountPoint("projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/import", ImportFolderTaskView.as_view()),
MountPoint("projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/checkforurl", CheckUrlTaskView.as_view()),
MountPoint("platforms/(?P<platform_name>[^/.]+)/verify", PlatformsVerifyTaskView.as_view()),
MountPoint("platforms", PlatformsTaskView.as_view()),
] """
def app_mount_points(self):
return [
MountPoint("$", HomeView(self)),
MountPoint("load_buttons.js$", LoadButtonsView(self)),
]

Wyświetl plik

@ -0,0 +1,128 @@
import React, { Component, Fragment } from "react";
import PropTypes from 'prop-types';
import ResizeModes from 'webodm/classes/ResizeModes';
import PlatformSelectButton from "./components/PlatformSelectButton";
import PlatformDialog from "./components/PlatformDialog";
import LibraryDialog from "./components/LibraryDialog";
import ErrorDialog from "./components/ErrorDialog";
import ConfigureNewTaskDialog from "./components/ConfigureNewTaskDialog";
export default class TaskView extends Component {
static propTypes = {
projectId: PropTypes.number.isRequired,
apiURL: PropTypes.string.isRequired,
onNewTaskAdded: PropTypes.func.isRequired,
}
state = {
error: "",
currentPlatform: null,
selectedFolder: null,
platforms: [],
};
componentDidMount() {
$.getJSON(`${this.props.apiURL}/platforms/`)
.done(data => {
this.setState({platforms: data.platforms});
})
.fail(() => {
this.onErrorInDialog("Failed to find available platforms")
})
}
onSelectPlatform = platform => this.setState({ currentPlatform: platform });
onSelectFolder = folder => this.setState({ selectedFolder: folder });
onHideDialog = () => this.setState({ currentPlatform: null, selectedFolder: null, taskId: null });
onSaveTask = taskInfo => {
// Create task
const formData = {
name: taskInfo.name,
options: taskInfo.options,
processing_node: taskInfo.selectedNode.id,
auto_processing_node: taskInfo.selectedNode.key == "auto",
partial: true
};
if (taskInfo.resizeMode === ResizeModes.YES){
formData.resize_to = taskInfo.resizeSize;
}
$.ajax({
url: `/api/projects/${this.props.projectId}/tasks/`,
contentType: 'application/json',
data: JSON.stringify(formData),
dataType: 'json',
type: 'POST'
}).done((task) => {
$.ajax({
url: `${this.props.apiURL}/projects/${this.props.projectId}/tasks/${task.id}/import`,
contentType: 'application/json',
data: JSON.stringify({platform: this.state.currentPlatform.name, selectedFolderUrl: this.state.selectedFolder.url}),
dataType: 'json',
type: 'POST'
}).done(() => {
this.onHideDialog();
this.props.onNewTaskAdded();
}).fail(error => {
this.onErrorInDialog("Failed to start importing.");
});
}).fail(() => {
this.onErrorInDialog("Cannot create new task. Please try again later.");
});
}
onErrorInDialog = msg => {
this.setState({ error: msg });
this.onHideDialog();
};
render() {
const {
currentPlatform,
error,
selectedFolder,
platforms,
} = this.state;
return (
<Fragment>
{error ?
<ErrorDialog errorMessage={error} />
: ""}
<PlatformSelectButton
platforms={platforms}
onSelect={this.onSelectPlatform}
/>
{selectedFolder === null ?
<Fragment>
<PlatformDialog
show={selectedFolder === null}
platform={currentPlatform}
apiURL={this.props.apiURL}
onHide={this.onHideDialog}
onSubmit={this.onSelectFolder}
/>
<LibraryDialog
show={selectedFolder === null}
platform={currentPlatform}
apiURL={this.props.apiURL}
onHide={this.onHideDialog}
onSubmit={this.onSelectFolder}
/>
</Fragment>
:
<ConfigureNewTaskDialog
show={selectedFolder !== null}
folder={selectedFolder}
platform={currentPlatform}
onHide={this.onHideDialog}
onSaveTask={this.onSaveTask}
/>
}
</Fragment>
);
}
}

Wyświetl plik

@ -0,0 +1,26 @@
import React, { Component, Fragment } from "react";
import PropTypes from 'prop-types';
import GoToFolderButton from "./components/GoToFolderButton";
import $ from "jquery"; // Fixes a AMD module definition error due to webpack
export default class TaskView extends Component {
static propTypes = {
folderUrl: PropTypes.string.isRequired,
}
render() {
const {
folderUrl,
} = this.props;
return (
<Fragment>
{folderUrl ?
<GoToFolderButton
folderUrl={folderUrl}
/>
: ""}
</Fragment>
);
}
}

Wyświetl plik

@ -0,0 +1,48 @@
import PropTypes from 'prop-types';
import { Component } from "react";
import { Modal } from "react-bootstrap";
import NewTaskPanel from "webodm/components/NewTaskPanel";
import "./ConfigureNewTaskDialog.scss";
export default class ConfigureNewTaskDialog extends Component {
static defaultProps = {
platform: null,
};
static propTypes = {
onHide: PropTypes.func.isRequired,
onSaveTask: PropTypes.func.isRequired,
folder: PropTypes.object,
platform: PropTypes.object,
}
render() {
const {
onHide,
onSaveTask,
platform,
folder,
} = this.props;
const title = "Import from " + (platform !== null ? platform.name : "Platform");
return (
<Modal className={"new-task"} onHide={onHide} show={true}>
<Modal.Header closeButton>
<Modal.Title>
{title}
</Modal.Title>
</Modal.Header>
<Modal.Body>
<NewTaskPanel
onSave={onSaveTask}
onCancel={onHide}
filesCount={folder ? folder.images_count : 0}
getFiles={() => []}
showResize={false}
suggestedTaskName={folder ? folder.name : null}
/>
</Modal.Body>
</Modal>
);
}
}

Wyświetl plik

@ -0,0 +1,7 @@
.modal-backdrop {
z-index: 100000 !important;
}
.new-task.modal button i {
margin-right: 1em;
}

Wyświetl plik

@ -0,0 +1,35 @@
import PropTypes from 'prop-types';
import { Component } from "react";
import { Modal } from "react-bootstrap";
import "./ErrorDialog.scss";
export default class ErrorDialog extends Component {
static propTypes = {
errorMessage: PropTypes.string.isRequired,
}
constructor(props){
super(props);
this.state = { show: true };
}
handleOnHide = () => this.setState({show: false});
render() {
const { errorMessage } = this.props;
return (
<Modal show={this.state.show}>
<Modal.Header closeButton onHide={this.handleOnHide}>
<Modal.Title>
There was an error with Cloud Import :(
</Modal.Title>
</Modal.Header>
<Modal.Body>
{ errorMessage }
</Modal.Body>
</Modal>
);
}
}

Wyświetl plik

@ -0,0 +1,3 @@
.modal-backdrop {
z-index: 100000 !important;
}

Wyświetl plik

@ -0,0 +1,26 @@
import React, { Component, Fragment } from "react";
import { Button } from "react-bootstrap";
import "./GoToFolderButton.scss";
export default class GoToFolderButton extends Component {
static defaultProps = {
folderUrl: null,
};
handleClick = () => window.open(this.props.folderUrl, '_blank');;
render() {
return (
<Button
bsStyle={"primary"}
bsSize={"small"}
onClick={this.handleClick}
>
<i className={"fa fa-folder icon"} />
Go To Import Folder
</Button>
);
}
}

Wyświetl plik

@ -0,0 +1,3 @@
.icon{
margin-right: 0.5em;
}

Wyświetl plik

@ -0,0 +1,94 @@
import PropTypes from 'prop-types';
import { Component } from "react";
import { Modal, Button } from "react-bootstrap";
import Select from 'react-select';
import "./LibraryDialog.scss";
export default class LibraryDialog extends Component {
static defaultProps = {
platform: null,
};
static propTypes = {
onHide: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
platform: PropTypes.object,
apiURL: PropTypes.string.isRequired,
}
constructor(props){
super(props);
this.state = {
availableFolders: [],
selectedFolder: null,
loadingFolders: true,
error: "",
};
}
componentDidUpdate(){
if (this.props.platform !== null && this.props.platform.type == "library" && this.state.loadingFolders){
$.get(`${this.props.apiURL}/cloudlibrary/${this.props.platform.name}/listfolders`)
.done(result => {
result.folders.forEach(album => {
album.label = `${album.name} (${album.images_count} images)`;
album.value = album.url;
})
this.setState({availableFolders: result.folders});
})
.fail((error) => {
this.setState({loadingFolders: false, error: "Cannot load folders. Check your internet connection."});
})
.always(() => {
this.setState({loadingFolders: false});
});
}
}
handleSelectFolder = (e) => this.setState({selectedFolder: e});
handleSubmit = e => this.props.onSubmit(this.state.selectedFolder);
render() {
const {
onHide,
platform,
} = this.props;
const title = "Import from " + (platform !== null ? platform.name : "Platform");
const isVisible = platform !== null && platform.type === "library";
return (
<Modal className={"folder-select"} onHide={onHide} show={isVisible}>
<Modal.Header closeButton>
<Modal.Title>
{title}
</Modal.Title>
</Modal.Header>
<Modal.Body bsClass="my-modal">
<p>Import the images from your <a target="_blank" href={platform === null ? "" : platform.server_url}>{platform === null ? "" : platform.name} library</a> into a new task.</p>
<Select
className="basic-single"
classNamePrefix="select"
isLoading={this.state.loadingFolders}
isClearable={false}
isSearchable={true}
onChange={this.handleSelectFolder}
options={this.state.availableFolders}
placeholder={this.state.loadingFolders ? "Fetching Piwigo albums..." : "Please select a Piwigo album"}
name="options"
/>
</Modal.Body>
<Modal.Footer>
<Button onClick={onHide}>Close</Button>
<Button
bsStyle="primary"
disabled={this.state.selectedFolder === null}
onClick={this.handleSubmit}
>
<i className={"fa fa-upload"} />
Import
</Button>
</Modal.Footer>
</Modal>
);
}
}

Wyświetl plik

@ -0,0 +1,13 @@
.modal-backdrop {
z-index: 100000 !important;
}
.folder-select.modal button i {
margin-right: 1em;
}
.my-modal {
max-height: calc(100vh - 220px);
position: relative;
padding: 20px;
}

Wyświetl plik

@ -0,0 +1,97 @@
import PropTypes from 'prop-types';
import { Component } from "react";
import { Modal, Button, FormGroup, ControlLabel, FormControl, HelpBlock } from "react-bootstrap";
import "./PlatformDialog.scss";
export default class PlatformDialog extends Component {
static defaultProps = {
platform: null,
};
static propTypes = {
onHide: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
platform: PropTypes.object,
apiURL: PropTypes.string.isRequired,
}
constructor(props){
super(props);
this.state = {
folderUrl: "",
fetchedFolder: null,
fetching: false,
};
}
handleChange = e => {
this.setState({folderUrl: e.target.value});
this.verifyFolderUrl(e.target.value);
}
handleSubmit = e => this.props.onSubmit(this.state.fetchedFolder);
verifyFolderUrl = (folderUrl) => {
if (this.props.platform == null) {
this.setState({fetching: false, fetchedFolder: null});
} else {
this.setState({fetching: true});
$.getJSON(`${this.props.apiURL}/platforms/${this.props.platform.name}/verify`, {folderUrl: folderUrl})
.done(data => this.setState({fetching: false, fetchedFolder: data.folder}))
.fail(() => this.setState({fetching: false, fetchedFolder: null}))
}
}
getValidationState = () => {
if (this.state.folderUrl === "" || this.state.fetching === true) {
return null;
} else if (this.state.fetchedFolder == null){
return "error";
} else {
return "success";
}
}
render() {
const {
onHide,
platform,
} = this.props;
const title = "Import from " + (platform !== null ? platform.name : "Platform");
const isVisible = platform !== null && platform.type === "platform";
return (
<Modal className={"folder-select"} onHide={onHide} show={isVisible}>
<Modal.Header closeButton>
<Modal.Title>
{title}
</Modal.Title>
</Modal.Header>
<Modal.Body>
<form>
<FormGroup controlId="folderUrl" validationState={this.getValidationState()}>
<ControlLabel>Folder/Album URL</ControlLabel>
<FormControl
type="url"
placeholder={platform !== null ? platform.folder_url_example : "Enter a folder url"}
onChange={this.handleChange}
/>
<FormControl.Feedback />
<HelpBlock>Please enter a valid folder/album URL. We will import all images from that folder.</HelpBlock>
</FormGroup>
</form>
</Modal.Body>
<Modal.Footer>
<Button onClick={onHide}>Close</Button>
<Button
bsStyle="primary"
disabled={this.state.fetchedFolder === null}
onClick={this.handleSubmit}
>
<i className={"fa fa-upload"} />
Import
</Button>
</Modal.Footer>
</Modal>
);
}
}

Wyświetl plik

@ -0,0 +1,7 @@
.modal-backdrop {
z-index: 100000 !important;
}
.folder-select.modal button i {
margin-right: 1em;
}

Wyświetl plik

@ -0,0 +1,55 @@
import React, { PureComponent, Fragment } from "react";
import { DropdownButton, MenuItem } from "react-bootstrap";
import "./PlatformSelectButton.scss";
export default class PlatformSelectButton extends PureComponent {
static defaultProps = {
platforms: [],
onSelect: () => {}
};
handleClick = platform => () => this.props.onSelect(platform);
render() {
const {
platforms,
onSelect,
} = this.props;
const menuItems = platforms
.map(platform => (
<MenuItem
key={platform.name}
tag={"a"}
onClick={this.handleClick(platform)}
>
<Fragment>
{" "}
{platform.name}
</Fragment>
</MenuItem>
));
const title = (
<Fragment>
<i className={"fa fa-cloud-download-alt fa-cloud-import"} />
Cloud Import
</Fragment>
);
return (
<DropdownButton
id={"platformsDropdown"}
bsStyle={"default"}
bsSize={"small"}
className={"platform-btn"}
title={title}
>
{menuItems}
</DropdownButton>
);
}
}

Wyświetl plik

@ -0,0 +1,21 @@
.platform-btn {
.fa-cloud-import {
margin-right: 0.5em;
}
.caret {
margin-left: 1em;
}
.btn-sm {
border-radius: 0;
}
}
.platforms-dropdowns {
display: inline-block;
.dropdown {
float: none;
}
}

Wyświetl plik

@ -0,0 +1,6 @@
{
"dependencies": {
"react-bootstrap": "^0.32.4",
"react-select": "^3.0.4"
}
}

Wyświetl plik

@ -0,0 +1,31 @@
{% extends "app/plugins/templates/base.html" %}
{% load bootstrap_extras %}
{% block content %}
<h3>Welcome to Cloud Import!</h3>
<h5><strong>Instructions</strong></h5>
On this screen, you will be able to configure everything that is necessary for your different platforms.<BR/>
You might need to set up a server URL, an authentication token or something else. If so, this is the place!
<BR/>
<BR/>
<h3>Platforms</h3>
<form action="" method="post" class="oam-form">
{% csrf_token %}
{% regroup form by field.group as field_groups %}
{% for field_group in field_groups %}
<h4><strong>{{field_group.grouper}}</strong></h4>
{% for field in field_group.list %}
<div class="form-group {% if field.errors %}has-error{% endif %}">
<label for="{{ field.id_for_label }}" class="control-label">{{ field.label }}</label>
{{ field|with_class:'form-control' }}
{% if field.errors %}
<span class='text-danger'>{{ field.errors|join:'<br />' }}</span>
{% elif field.help_text %}
<span class="help-block ">{{ field.help_text }}</span>
{% endif %}
</div>
{% endfor %}
{% endfor %}
<button type="submit" class="btn btn-primary"><i class="fa fa-save fa-fw"></i> Save Configuration</button>
</form>
{% endblock %}

Wyświetl plik

@ -0,0 +1,30 @@
PluginsAPI.Dashboard.addNewTaskButton(
["cloudimport/build/ImportView.js"],
function(args, ImportView) {
return React.createElement(ImportView, {
onNewTaskAdded: args.onNewTaskAdded,
projectId: args.projectId,
apiURL: "{{ api_url }}",
});
}
);
PluginsAPI.Dashboard.addTaskActionButton(
["cloudimport/build/TaskView.js", "cloudimport/build/TaskView.css"],
function(args, TaskView) {
var reactElement;
$.ajax({
url: "{{ api_url }}/projects/" + args.task.project + "/tasks/" + args.task.id + "/checkforurl",
dataType: 'json',
async: false,
success: function(data) {
if (data.folder_url) {
reactElement = React.createElement(TaskView, {
folderUrl: data.folder_url,
});
}
}
});
return reactElement;
}
);