kopia lustrzana https://github.com/OpenDroneMap/WebODM
commit
7ee8fc568c
|
@ -1,9 +1,13 @@
|
|||
from guardian.shortcuts import get_perms
|
||||
from rest_framework import serializers, viewsets
|
||||
from rest_framework.decorators import detail_route
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
|
||||
from app import models
|
||||
from .tasks import TaskIDsSerializer
|
||||
|
||||
from .common import get_and_check_project
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
class ProjectSerializer(serializers.ModelSerializer):
|
||||
tasks = TaskIDsSerializer(many=True, read_only=True)
|
||||
|
@ -37,3 +41,22 @@ class ProjectViewSet(viewsets.ModelViewSet):
|
|||
serializer_class = ProjectSerializer
|
||||
queryset = models.Project.objects.prefetch_related('task_set').filter(deleting=False).order_by('-created_at')
|
||||
ordering_fields = '__all__'
|
||||
|
||||
# Disable pagination when not requesting any page
|
||||
def paginate_queryset(self, queryset):
|
||||
if self.paginator and self.request.query_params.get(self.paginator.page_query_param, None) is None:
|
||||
return None
|
||||
return super().paginate_queryset(queryset)
|
||||
|
||||
@detail_route(methods=['post'])
|
||||
def duplicate(self, request, pk=None):
|
||||
"""
|
||||
Duplicate a task
|
||||
"""
|
||||
project = get_and_check_project(request, pk, ('change_project', ))
|
||||
|
||||
new_project = project.duplicate()
|
||||
if new_project:
|
||||
return Response({'success': True, 'project': ProjectSerializer(new_project).data}, status=status.HTTP_200_OK)
|
||||
else:
|
||||
return Response({'error': _("Cannot duplicate project")}, status=status.HTTP_200_OK)
|
||||
|
|
|
@ -210,6 +210,23 @@ class TaskViewSet(viewsets.ViewSet):
|
|||
|
||||
return Response({'success': True}, status=status.HTTP_200_OK)
|
||||
|
||||
@detail_route(methods=['post'])
|
||||
def duplicate(self, request, pk=None, project_pk=None):
|
||||
"""
|
||||
Duplicate a task
|
||||
"""
|
||||
get_and_check_project(request, project_pk, ('change_project', ))
|
||||
try:
|
||||
task = self.queryset.get(pk=pk, project=project_pk)
|
||||
except (ObjectDoesNotExist, ValidationError):
|
||||
raise exceptions.NotFound()
|
||||
|
||||
new_task = task.duplicate()
|
||||
if new_task:
|
||||
return Response({'success': True, 'task': TaskSerializer(new_task).data}, status=status.HTTP_200_OK)
|
||||
else:
|
||||
return Response({'error': _("Cannot duplicate task")}, status=status.HTTP_200_OK)
|
||||
|
||||
def create(self, request, project_pk=None):
|
||||
project = get_and_check_project(request, project_pk, ('change_project', ))
|
||||
|
||||
|
|
|
@ -9,7 +9,8 @@ from django.utils import timezone
|
|||
from guardian.models import GroupObjectPermissionBase
|
||||
from guardian.models import UserObjectPermissionBase
|
||||
from guardian.shortcuts import get_perms_for_model, assign_perm
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _, gettext
|
||||
from django.db import transaction
|
||||
|
||||
from app import pending_actions
|
||||
|
||||
|
@ -51,6 +52,33 @@ class Project(models.Model):
|
|||
status=status_codes.COMPLETED
|
||||
).filter(Q(orthophoto_extent__isnull=False) | Q(dsm_extent__isnull=False) | Q(dtm_extent__isnull=False))
|
||||
.only('id', 'project_id')]
|
||||
|
||||
def duplicate(self):
|
||||
try:
|
||||
with transaction.atomic():
|
||||
project = Project.objects.get(pk=self.pk)
|
||||
project.pk = None
|
||||
project.name = gettext('Copy of %(task)s') % {'task': self.name}
|
||||
project.created_at = timezone.now()
|
||||
project.save()
|
||||
project.refresh_from_db()
|
||||
|
||||
for task in self.task_set.all():
|
||||
new_task = task.duplicate(set_new_name=False)
|
||||
if not new_task:
|
||||
raise Exception("Failed to duplicate {}".format(new_task))
|
||||
|
||||
# Move/Assign to new duplicate
|
||||
new_task.project = project
|
||||
new_task.save()
|
||||
|
||||
return project
|
||||
except Exception as e:
|
||||
logger.warning("Cannot duplicate project: {}".format(str(e)))
|
||||
|
||||
return False
|
||||
|
||||
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Project")
|
||||
|
|
|
@ -310,8 +310,26 @@ class Task(models.Model):
|
|||
self.move_assets(self.__original_project_id, self.project.id)
|
||||
self.__original_project_id = self.project.id
|
||||
|
||||
# Autovalidate on save
|
||||
self.full_clean()
|
||||
# Manually validate the fields we want,
|
||||
# since Django's clean_fields() method obliterates
|
||||
# our foreign keys without explanation :/
|
||||
errors = {}
|
||||
for f in self._meta.fields:
|
||||
if f.attname in ["options"]:
|
||||
raw_value = getattr(self, f.attname)
|
||||
if f.blank and raw_value in f.empty_values:
|
||||
continue
|
||||
|
||||
try:
|
||||
setattr(self, f.attname, f.clean(raw_value, self))
|
||||
except ValidationError as e:
|
||||
errors[f.name] = e.error_list
|
||||
|
||||
if errors:
|
||||
raise ValidationError(errors)
|
||||
|
||||
self.clean()
|
||||
self.validate_unique()
|
||||
|
||||
super(Task, self).save(*args, **kwargs)
|
||||
|
||||
|
@ -376,6 +394,44 @@ class Task(models.Model):
|
|||
else:
|
||||
return {}
|
||||
|
||||
def duplicate(self, set_new_name=True):
|
||||
try:
|
||||
with transaction.atomic():
|
||||
task = Task.objects.get(pk=self.pk)
|
||||
task.pk = None
|
||||
if set_new_name:
|
||||
task.name = gettext('Copy of %(task)s') % {'task': self.name}
|
||||
task.created_at = timezone.now()
|
||||
task.save()
|
||||
task.refresh_from_db()
|
||||
|
||||
logger.info("Duplicating {} to {}".format(self, task))
|
||||
|
||||
for img in self.imageupload_set.all():
|
||||
img.pk = None
|
||||
img.task = task
|
||||
|
||||
prev_name = img.image.name
|
||||
img.image.name = assets_directory_path(task.id, task.project.id,
|
||||
os.path.basename(img.image.name))
|
||||
|
||||
img.save()
|
||||
|
||||
if os.path.isdir(self.task_path()):
|
||||
try:
|
||||
# Try to use hard links first
|
||||
shutil.copytree(self.task_path(), task.task_path(), copy_function=os.link)
|
||||
except Exception as e:
|
||||
logger.warning("Cannot duplicate task using hard links, will use normal copy instead: {}".format(str(e)))
|
||||
shutil.copytree(self.task_path(), task.task_path())
|
||||
else:
|
||||
logger.warning("Task {} doesn't have folder, will skip copying".format(self))
|
||||
return task
|
||||
except Exception as e:
|
||||
logger.warning("Cannot duplicate task: {}".format(str(e)))
|
||||
|
||||
return False
|
||||
|
||||
def get_asset_download_path(self, asset):
|
||||
"""
|
||||
Get the path to an asset download
|
||||
|
|
|
@ -123,7 +123,7 @@ def build_plugins():
|
|||
|
||||
# Check for webpack.config.js (if we need to build it)
|
||||
if plugin.path_exists("public/webpack.config.js"):
|
||||
if settings.DEV and webpack_watch_process_count() <= 2:
|
||||
if settings.DEV and webpack_watch_process_count() <= 2 and settings.DEV_WATCH_PLUGINS:
|
||||
logger.info("Running webpack with watcher for {}".format(plugin.get_name()))
|
||||
subprocess.Popen(['webpack-cli', '--watch'], cwd=plugin.get_path("public"))
|
||||
elif not plugin.path_exists("public/build"):
|
||||
|
|
|
@ -51,6 +51,11 @@ export default {
|
|||
action: "odm_report",
|
||||
label: _("Report"),
|
||||
icon: "far fa-file-alt"
|
||||
},
|
||||
{
|
||||
action: "odm_postprocess",
|
||||
label: _("Postprocess"),
|
||||
icon: "fa fa-cog"
|
||||
}
|
||||
];
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ let statusCodes = {
|
|||
icon: "far fa-hourglass fa-fw"
|
||||
},
|
||||
[RUNNING]: {
|
||||
descr: _("Running"),
|
||||
descr: _("Processing"),
|
||||
icon: "fa fa-cog fa-spin fa-fw"
|
||||
},
|
||||
[FAILED]: {
|
||||
|
|
|
@ -109,13 +109,13 @@ class EditPresetDialog extends React.Component {
|
|||
deleteWarning={false}
|
||||
deleteAction={(this.props.preset.id !== -1 && !this.props.preset.system) ? this.props.deleteAction : undefined}>
|
||||
{!this.isCustomPreset() ?
|
||||
[<div className="row preset-name">
|
||||
[<div className="row preset-name" key="preset">
|
||||
<label className="col-sm-2 control-label">{_("Name")}</label>
|
||||
<div className="col-sm-10" style={{marginRight: "40px"}}>
|
||||
<input type="text" className="form-control" ref={(domNode) => { this.nameInput = domNode; }} value={this.state.name} onChange={this.handleChange('name')} />
|
||||
</div>
|
||||
</div>,
|
||||
<hr/>]
|
||||
<hr key="hr"/>]
|
||||
: ""}
|
||||
|
||||
<button type="submit" className="btn btn-default search-toggle btn-sm" title={_("Search")} onClick={this.toggleSearchControl}><i className="fa fa-filter"></i></button>
|
||||
|
|
|
@ -1,23 +1,28 @@
|
|||
import React from 'react';
|
||||
import FormDialog from './FormDialog';
|
||||
import PropTypes from 'prop-types';
|
||||
import ErrorMessage from './ErrorMessage';
|
||||
import { _ } from '../classes/gettext';
|
||||
|
||||
class EditProjectDialog extends React.Component {
|
||||
static defaultProps = {
|
||||
projectName: "",
|
||||
projectDescr: "",
|
||||
projectId: -1,
|
||||
title: _("New Project"),
|
||||
saveLabel: _("Create Project"),
|
||||
savingLabel: _("Creating project..."),
|
||||
saveIcon: "glyphicon glyphicon-plus",
|
||||
deleteWarning: _("All tasks, images and models associated with this project will be permanently deleted. Are you sure you want to continue?"),
|
||||
show: false
|
||||
show: false,
|
||||
showDuplicate: false,
|
||||
onDuplicated: () => {}
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
projectName: PropTypes.string,
|
||||
projectDescr: PropTypes.string,
|
||||
projectId: PropTypes.number,
|
||||
saveAction: PropTypes.func.isRequired,
|
||||
onShow: PropTypes.func,
|
||||
deleteAction: PropTypes.func,
|
||||
|
@ -26,7 +31,9 @@ class EditProjectDialog extends React.Component {
|
|||
savingLabel: PropTypes.string,
|
||||
saveIcon: PropTypes.string,
|
||||
deleteWarning: PropTypes.string,
|
||||
show: PropTypes.bool
|
||||
show: PropTypes.bool,
|
||||
showDuplicate: PropTypes.bool,
|
||||
onDuplicated: PropTypes.func
|
||||
};
|
||||
|
||||
constructor(props){
|
||||
|
@ -34,7 +41,9 @@ class EditProjectDialog extends React.Component {
|
|||
|
||||
this.state = {
|
||||
name: props.projectName,
|
||||
descr: props.projectDescr !== null ? props.projectDescr : ""
|
||||
descr: props.projectDescr !== null ? props.projectDescr : "",
|
||||
duplicating: false,
|
||||
error: ""
|
||||
};
|
||||
|
||||
this.reset = this.reset.bind(this);
|
||||
|
@ -46,12 +55,17 @@ class EditProjectDialog extends React.Component {
|
|||
reset(){
|
||||
this.setState({
|
||||
name: this.props.projectName,
|
||||
descr: this.props.projectDescr
|
||||
descr: this.props.projectDescr,
|
||||
duplicating: false,
|
||||
error: ""
|
||||
});
|
||||
}
|
||||
|
||||
getFormData(){
|
||||
return this.state;
|
||||
return {
|
||||
name: this.state.name,
|
||||
descr: this.state.descr,
|
||||
};
|
||||
}
|
||||
|
||||
onShow(){
|
||||
|
@ -64,6 +78,11 @@ class EditProjectDialog extends React.Component {
|
|||
|
||||
hide(){
|
||||
this.dialog.hide();
|
||||
|
||||
if (this.duplicateRequest){
|
||||
this.duplicateRequest.abort();
|
||||
this.duplicateRequest = null;
|
||||
}
|
||||
}
|
||||
|
||||
handleChange(field){
|
||||
|
@ -74,13 +93,39 @@ class EditProjectDialog extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
handleDuplicate = () => {
|
||||
this.setState({duplicating: true});
|
||||
this.duplicateRequest = $.post(`/api/projects/${this.props.projectId}/duplicate/`)
|
||||
.done(json => {
|
||||
if (json.success){
|
||||
this.hide();
|
||||
this.props.onDuplicated(json.project);
|
||||
}else{
|
||||
this.setState({
|
||||
error: json.error || _("Cannot complete operation.")
|
||||
});
|
||||
}
|
||||
})
|
||||
.fail(() => {
|
||||
this.setState({
|
||||
error: _("Cannot complete operation."),
|
||||
});
|
||||
})
|
||||
.always(() => {
|
||||
this.setState({duplicating: false});
|
||||
this.duplicateRequest = null;
|
||||
});
|
||||
}
|
||||
|
||||
render(){
|
||||
return (
|
||||
<FormDialog {...this.props}
|
||||
getFormData={this.getFormData}
|
||||
<FormDialog {...this.props}
|
||||
getFormData={this.getFormData}
|
||||
reset={this.reset}
|
||||
onShow={this.onShow}
|
||||
leftButtons={this.props.showDuplicate ? [<button disabled={this.duplicating} onClick={this.handleDuplicate} className="btn btn-default"><i className={"fa " + (this.state.duplicating ? "fa-circle-notch fa-spin fa-fw" : "fa-copy")}></i> Duplicate</button>] : undefined}
|
||||
ref={(domNode) => { this.dialog = domNode; }}>
|
||||
<ErrorMessage bind={[this, "error"]} />
|
||||
<div className="form-group">
|
||||
<label className="col-sm-2 control-label">{_("Name")}</label>
|
||||
<div className="col-sm-10">
|
||||
|
|
|
@ -17,7 +17,7 @@ class FormDialog extends React.Component {
|
|||
|
||||
static propTypes = {
|
||||
getFormData: PropTypes.func.isRequired,
|
||||
reset: PropTypes.func.isRequired,
|
||||
reset: PropTypes.func,
|
||||
saveAction: PropTypes.func.isRequired,
|
||||
onShow: PropTypes.func,
|
||||
onHide: PropTypes.func,
|
||||
|
@ -88,13 +88,17 @@ class FormDialog extends React.Component {
|
|||
}
|
||||
|
||||
show(){
|
||||
this.props.reset();
|
||||
if (this.props.reset) this.props.reset();
|
||||
this.setState({showModal: true, saving: false, error: ""});
|
||||
}
|
||||
|
||||
hide(){
|
||||
this.setState({showModal: false});
|
||||
if (this.props.onHide) this.props.onHide();
|
||||
if (this.serverRequest){
|
||||
this.serverRequest.abort();
|
||||
this.serverRequest = null;
|
||||
}
|
||||
}
|
||||
|
||||
handleSave(e){
|
||||
|
@ -105,13 +109,20 @@ class FormDialog extends React.Component {
|
|||
let formData = {};
|
||||
if (this.props.getFormData) formData = this.props.getFormData();
|
||||
|
||||
this.props.saveAction(formData).fail(e => {
|
||||
this.setState({error: e.message || (e.responseJSON || {}).detail || e.responseText || _("Could not apply changes")});
|
||||
}).always(() => {
|
||||
this.serverRequest = this.props.saveAction(formData);
|
||||
if (this.serverRequest){
|
||||
this.serverRequest.fail(e => {
|
||||
this.setState({error: e.message || (e.responseJSON || {}).detail || e.responseText || _("Could not apply changes")});
|
||||
}).always(() => {
|
||||
this.setState({saving: false});
|
||||
this.serverRequest = null;
|
||||
}).done(() => {
|
||||
this.hide();
|
||||
});
|
||||
}else{
|
||||
this.setState({saving: false});
|
||||
}).done(() => {
|
||||
this.hide();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleDelete(){
|
||||
|
@ -129,6 +140,25 @@ class FormDialog extends React.Component {
|
|||
}
|
||||
|
||||
render(){
|
||||
let leftButtons = [];
|
||||
if (this.props.deleteAction){
|
||||
leftButtons.push(<button
|
||||
disabled={this.state.deleting}
|
||||
className="btn btn-danger"
|
||||
onClick={this.handleDelete}>
|
||||
{this.state.deleting ?
|
||||
<span>
|
||||
<i className="fa fa-circle-notch fa-spin"></i> {_("Deleting...")}
|
||||
</span>
|
||||
: <span>
|
||||
<i className="fa fa-trash"></i> {_("Delete")}
|
||||
</span>}
|
||||
</button>);
|
||||
}
|
||||
if (this.props.leftButtons){
|
||||
leftButtons = leftButtons.concat(this.props.leftButtons);
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={this.setModal}
|
||||
className="modal form-dialog" tabIndex="-1"
|
||||
|
@ -159,20 +189,10 @@ class FormDialog extends React.Component {
|
|||
</span>}
|
||||
</button>
|
||||
</div>
|
||||
{this.props.deleteAction ?
|
||||
|
||||
{leftButtons.length > 0 ?
|
||||
<div className="text-left">
|
||||
<button
|
||||
disabled={this.state.deleting}
|
||||
className="btn btn-danger"
|
||||
onClick={this.handleDelete}>
|
||||
{this.state.deleting ?
|
||||
<span>
|
||||
<i className="fa fa-circle-notch fa-spin"></i> {_("Deleting...")}
|
||||
</span>
|
||||
: <span>
|
||||
<i className="glyphicon glyphicon-trash"></i> {_("Delete")}
|
||||
</span>}
|
||||
</button>
|
||||
{leftButtons}
|
||||
</div>
|
||||
: ""}
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
import React from 'react';
|
||||
import FormDialog from './FormDialog';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _ } from '../classes/gettext';
|
||||
import $ from 'jquery';
|
||||
|
||||
class MoveTaskDialog extends React.Component {
|
||||
static defaultProps = {
|
||||
title: _("Move Task"),
|
||||
saveLabel: _("Save Changes"),
|
||||
savingLabel: _("Moving..."),
|
||||
saveIcon: "far fa-edit",
|
||||
show: true
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
task: PropTypes.object.isRequired,
|
||||
saveAction: PropTypes.func.isRequired,
|
||||
title: PropTypes.string,
|
||||
saveLabel: PropTypes.string,
|
||||
savingLabel: PropTypes.string,
|
||||
saveIcon: PropTypes.string,
|
||||
show: PropTypes.bool
|
||||
};
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
projectId: props.task.project,
|
||||
projects: [],
|
||||
loading: true
|
||||
};
|
||||
|
||||
this.getFormData = this.getFormData.bind(this);
|
||||
this.onShow = this.onShow.bind(this);
|
||||
}
|
||||
|
||||
getFormData(){
|
||||
return {project: this.state.projectId};
|
||||
}
|
||||
|
||||
onShow(){
|
||||
this.setState({loading: true, projects: []});
|
||||
|
||||
// Load projects from API
|
||||
this.serverRequest = $.getJSON(`/api/projects/?ordering=-created_at`, json => {
|
||||
this.setState({
|
||||
projects: json.filter(p => p.permissions.indexOf("add") !== -1)
|
||||
});
|
||||
})
|
||||
.fail((jqXHR, textStatus, errorThrown) => {
|
||||
this.dialog.setState({
|
||||
error: interpolate(_("Could not load projects list: %(error)s"), {error: textStatus})
|
||||
});
|
||||
})
|
||||
.always(() => {
|
||||
this.setState({loading: false});
|
||||
this.serverRequest = null;
|
||||
});
|
||||
}
|
||||
|
||||
show(){
|
||||
this.dialog.show();
|
||||
}
|
||||
|
||||
hide(){
|
||||
this.dialog.hide();
|
||||
}
|
||||
|
||||
componentWillUnmount(){
|
||||
if (this.serverRquest) this.serverRquest.abort();
|
||||
}
|
||||
|
||||
handleProjectChange = e => {
|
||||
this.setState({projectId: e.target.value});
|
||||
}
|
||||
|
||||
render(){
|
||||
return (
|
||||
<FormDialog {...this.props}
|
||||
getFormData={this.getFormData}
|
||||
onShow={this.onShow}
|
||||
ref={(domNode) => { this.dialog = domNode; }}>
|
||||
<div style={{minHeight: '50px'}}>
|
||||
{!this.state.loading ?
|
||||
<div className="form-group">
|
||||
<label className="col-sm-2 control-label">{_("Project")}</label>
|
||||
<div className="col-sm-10">
|
||||
<select className="form-control"
|
||||
value={this.state.projectId}
|
||||
onChange={this.handleProjectChange}>
|
||||
{this.state.projects.map(p =>
|
||||
<option value={p.id} key={p.id}>{p.name}</option>
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
: <i className="fa fa-circle-notch fa-spin fa-fw name-loading"></i>}
|
||||
</div>
|
||||
</FormDialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default MoveTaskDialog;
|
|
@ -85,6 +85,16 @@ class ProjectList extends Paginated {
|
|||
});
|
||||
}
|
||||
|
||||
handleTaskMoved = (task) => {
|
||||
if (this["projectListItem_" + task.project]){
|
||||
this["projectListItem_" + task.project].newTaskAdded();
|
||||
}
|
||||
}
|
||||
|
||||
handleProjectDuplicated = () => {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.loading){
|
||||
return (<div className="project-list text-center"><i className="fa fa-sync fa-spin fa-2x fa-fw"></i></div>);
|
||||
|
@ -95,9 +105,12 @@ class ProjectList extends Paginated {
|
|||
<ul className={"list-group project-list " + (this.state.refreshing ? "refreshing" : "")}>
|
||||
{this.state.projects.map(p => (
|
||||
<ProjectListItem
|
||||
ref={(domNode) => { this["projectListItem_" + p.id] = domNode }}
|
||||
key={p.id}
|
||||
data={p}
|
||||
onDelete={this.handleDelete}
|
||||
onDelete={this.handleDelete}
|
||||
onTaskMoved={this.handleTaskMoved}
|
||||
onProjectDuplicated={this.handleProjectDuplicated}
|
||||
history={this.props.history} />
|
||||
))}
|
||||
</ul>
|
||||
|
|
|
@ -20,7 +20,9 @@ class ProjectListItem extends React.Component {
|
|||
static propTypes = {
|
||||
history: PropTypes.object.isRequired,
|
||||
data: PropTypes.object.isRequired, // project json
|
||||
onDelete: PropTypes.func
|
||||
onDelete: PropTypes.func,
|
||||
onTaskMoved: PropTypes.func,
|
||||
onProjectDuplicated: PropTypes.func
|
||||
}
|
||||
|
||||
constructor(props){
|
||||
|
@ -47,6 +49,7 @@ class ProjectListItem extends React.Component {
|
|||
this.handleEditProject = this.handleEditProject.bind(this);
|
||||
this.updateProject = this.updateProject.bind(this);
|
||||
this.taskDeleted = this.taskDeleted.bind(this);
|
||||
this.taskMoved = this.taskMoved.bind(this);
|
||||
this.hasPermission = this.hasPermission.bind(this);
|
||||
}
|
||||
|
||||
|
@ -304,6 +307,11 @@ class ProjectListItem extends React.Component {
|
|||
this.refresh();
|
||||
}
|
||||
|
||||
taskMoved(task){
|
||||
this.refresh();
|
||||
if (this.props.onTaskMoved) this.props.onTaskMoved(task);
|
||||
}
|
||||
|
||||
handleDelete(){
|
||||
return $.ajax({
|
||||
url: `/api/projects/${this.state.data.id}/`,
|
||||
|
@ -474,8 +482,11 @@ class ProjectListItem extends React.Component {
|
|||
saveLabel={_("Save Changes")}
|
||||
savingLabel={_("Saving changes...")}
|
||||
saveIcon="far fa-edit"
|
||||
showDuplicate={true}
|
||||
onDuplicated={this.props.onProjectDuplicated}
|
||||
projectName={data.name}
|
||||
projectDescr={data.description}
|
||||
projectId={data.id}
|
||||
saveAction={this.updateProject}
|
||||
deleteAction={this.hasPermission("delete") ? this.handleDelete : undefined}
|
||||
/>
|
||||
|
@ -570,6 +581,8 @@ class ProjectListItem extends React.Component {
|
|||
ref={this.setRef("taskList")}
|
||||
source={`/api/projects/${data.id}/tasks/?ordering=-created_at`}
|
||||
onDelete={this.taskDeleted}
|
||||
onTaskMoved={this.taskMoved}
|
||||
hasPermission={this.hasPermission}
|
||||
history={this.props.history}
|
||||
/> : ""}
|
||||
|
||||
|
|
|
@ -9,7 +9,9 @@ class TaskList extends React.Component {
|
|||
static propTypes = {
|
||||
history: PropTypes.object.isRequired,
|
||||
source: PropTypes.string.isRequired, // URL where to load task list
|
||||
onDelete: PropTypes.func
|
||||
onDelete: PropTypes.func,
|
||||
onTaskMoved: PropTypes.func,
|
||||
hasPermission: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
constructor(props){
|
||||
|
@ -69,6 +71,11 @@ class TaskList extends React.Component {
|
|||
if (this.props.onDelete) this.props.onDelete(id);
|
||||
}
|
||||
|
||||
moveTask = (task) => {
|
||||
this.refresh();
|
||||
if (this.props.onTaskMoved) this.props.onTaskMoved(task);
|
||||
}
|
||||
|
||||
render() {
|
||||
let message = "";
|
||||
if (this.state.loading){
|
||||
|
@ -76,7 +83,7 @@ class TaskList extends React.Component {
|
|||
}else if (this.state.error){
|
||||
message = (<span>{interpolate(_("Error: %(error)s"), {error: this.state.error})} <a href="javascript:void(0);" onClick={this.retry}>{_("Try again")}</a></span>);
|
||||
}else if (this.state.tasks.length === 0){
|
||||
message = (<span>{_("This project has no tasks. Create one by uploading some images!")}</span>);
|
||||
message = (<span></span>);
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -88,7 +95,10 @@ class TaskList extends React.Component {
|
|||
data={task}
|
||||
key={task.id}
|
||||
refreshInterval={3000}
|
||||
onDelete={this.deleteTask}
|
||||
onDelete={this.deleteTask}
|
||||
onMove={this.moveTask}
|
||||
onDuplicate={this.refresh}
|
||||
hasPermission={this.props.hasPermission}
|
||||
history={this.props.history} />
|
||||
))}
|
||||
</div>
|
||||
|
|
|
@ -9,6 +9,7 @@ import AssetDownloadButtons from './AssetDownloadButtons';
|
|||
import HistoryNav from '../classes/HistoryNav';
|
||||
import PropTypes from 'prop-types';
|
||||
import TaskPluginActionButtons from './TaskPluginActionButtons';
|
||||
import MoveTaskDialog from './MoveTaskDialog';
|
||||
import PipelineSteps from '../classes/PipelineSteps';
|
||||
import Css from '../classes/Css';
|
||||
import Trans from './Trans';
|
||||
|
@ -19,7 +20,10 @@ class TaskListItem extends React.Component {
|
|||
history: PropTypes.object.isRequired,
|
||||
data: PropTypes.object.isRequired, // task json
|
||||
refreshInterval: PropTypes.number, // how often to refresh info
|
||||
onDelete: PropTypes.func
|
||||
onDelete: PropTypes.func,
|
||||
onMove: PropTypes.func,
|
||||
onDuplicate: PropTypes.func,
|
||||
hasPermission: PropTypes.func
|
||||
}
|
||||
|
||||
constructor(props){
|
||||
|
@ -37,7 +41,9 @@ class TaskListItem extends React.Component {
|
|||
memoryError: false,
|
||||
friendlyTaskError: "",
|
||||
pluginActionButtons: [],
|
||||
view: "basic"
|
||||
view: "basic",
|
||||
showMoveDialog: false,
|
||||
actionLoading: false,
|
||||
}
|
||||
|
||||
for (let k in props.data){
|
||||
|
@ -194,11 +200,12 @@ class TaskListItem extends React.Component {
|
|||
).done(json => {
|
||||
if (json.success){
|
||||
this.refresh();
|
||||
if (options.success !== undefined) options.success();
|
||||
if (options.success !== undefined) options.success(json);
|
||||
}else{
|
||||
this.setState({
|
||||
actionError: json.error || options.defaultError || _("Cannot complete operation."),
|
||||
actionButtonsDisabled: false
|
||||
actionButtonsDisabled: false,
|
||||
expanded: true
|
||||
});
|
||||
}
|
||||
})
|
||||
|
@ -207,6 +214,9 @@ class TaskListItem extends React.Component {
|
|||
actionError: options.defaultError || _("Cannot complete operation."),
|
||||
actionButtonsDisabled: false
|
||||
});
|
||||
})
|
||||
.always(() => {
|
||||
if (options.always !== undefined) options.always();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -271,6 +281,23 @@ class TaskListItem extends React.Component {
|
|||
this.setAutoRefresh();
|
||||
}
|
||||
|
||||
handleMoveTask = () => {
|
||||
this.setState({showMoveDialog: true});
|
||||
}
|
||||
|
||||
handleDuplicateTask = () => {
|
||||
this.setState({actionLoading: true});
|
||||
this.genActionApiCall("duplicate", {
|
||||
success: (json) => {
|
||||
if (json.task){
|
||||
if (this.props.onDuplicate) this.props.onDuplicate(json.task);
|
||||
}
|
||||
},
|
||||
always: () => {
|
||||
this.setState({actionLoading: false});
|
||||
}})();
|
||||
}
|
||||
|
||||
getRestartSubmenuItems(){
|
||||
const { task } = this.state;
|
||||
|
||||
|
@ -362,6 +389,18 @@ class TaskListItem extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
moveTaskAction = (formData) => {
|
||||
if (formData.project !== this.state.task.project){
|
||||
return $.ajax({
|
||||
url: `/api/projects/${this.state.task.project}/tasks/${this.state.task.id}/`,
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(formData),
|
||||
dataType: 'json',
|
||||
type: 'PATCH'
|
||||
}).done(this.props.onMove);
|
||||
}else return false;
|
||||
}
|
||||
|
||||
render() {
|
||||
const task = this.state.task;
|
||||
const name = task.name !== null ? task.name : interpolate(_("Task #%(number)s"), { number: task.id });
|
||||
|
@ -373,6 +412,12 @@ class TaskListItem extends React.Component {
|
|||
if (!task.processing_node && !imported) status = _("Waiting for a node...");
|
||||
if (task.pending_action !== null) status = pendingActions.description(task.pending_action);
|
||||
|
||||
const disabled = this.state.actionButtonsDisabled ||
|
||||
([pendingActions.CANCEL,
|
||||
pendingActions.REMOVE,
|
||||
pendingActions.RESTART].indexOf(task.pending_action) !== -1);
|
||||
const editable = this.props.hasPermission("change") && [statusCodes.FAILED, statusCodes.COMPLETED, statusCodes.CANCELED].indexOf(task.status) !== -1;
|
||||
const actionLoading = this.state.actionLoading;
|
||||
|
||||
let expanded = "";
|
||||
if (this.state.expanded){
|
||||
|
@ -406,9 +451,7 @@ class TaskListItem extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
// Ability to change options
|
||||
if ([statusCodes.FAILED, statusCodes.COMPLETED, statusCodes.CANCELED].indexOf(task.status) !== -1 ||
|
||||
(!task.processing_node)){
|
||||
if (editable || (!task.processing_node)){
|
||||
addActionButton(_("Edit"), "btn-primary pull-right edit-button", "glyphicon glyphicon-pencil", () => {
|
||||
this.startEditing();
|
||||
}, {
|
||||
|
@ -417,12 +460,13 @@ class TaskListItem extends React.Component {
|
|||
}
|
||||
|
||||
if ([statusCodes.QUEUED, statusCodes.RUNNING, null].indexOf(task.status) !== -1 &&
|
||||
(task.processing_node || imported)){
|
||||
(task.processing_node || imported) && this.props.hasPermission("change")){
|
||||
addActionButton(_("Cancel"), "btn-primary", "glyphicon glyphicon-remove-circle", this.genActionApiCall("cancel", {defaultError: _("Cannot cancel task.")}));
|
||||
}
|
||||
|
||||
if ([statusCodes.FAILED, statusCodes.COMPLETED, statusCodes.CANCELED].indexOf(task.status) !== -1 &&
|
||||
task.processing_node &&
|
||||
this.props.hasPermission("change") &&
|
||||
!imported){
|
||||
// By default restart reruns every pipeline
|
||||
// step from the beginning
|
||||
|
@ -435,15 +479,12 @@ class TaskListItem extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
addActionButton(_("Delete"), "btn-danger", "glyphicon glyphicon-trash", this.genActionApiCall("remove", {
|
||||
confirm: _("All information related to this task, including images, maps and models will be deleted. Continue?"),
|
||||
defaultError: _("Cannot delete task.")
|
||||
}));
|
||||
|
||||
const disabled = this.state.actionButtonsDisabled ||
|
||||
([pendingActions.CANCEL,
|
||||
pendingActions.REMOVE,
|
||||
pendingActions.RESTART].indexOf(task.pending_action) !== -1);
|
||||
if (this.props.hasPermission("delete")){
|
||||
addActionButton(_("Delete"), "btn-danger", "fa fa-trash fa-fw", this.genActionApiCall("remove", {
|
||||
confirm: _("All information related to this task, including images, maps and models will be deleted. Continue?"),
|
||||
defaultError: _("Cannot delete task.")
|
||||
}));
|
||||
}
|
||||
|
||||
actionButtons = (<div className="action-buttons">
|
||||
{task.status === statusCodes.COMPLETED ?
|
||||
|
@ -517,11 +558,11 @@ class TaskListItem extends React.Component {
|
|||
|
||||
{stats && stats.gsd ?
|
||||
<div className="labels">
|
||||
<strong>{_("Average GSD:")} </strong> {stats.gsd.toFixed(2)} cm<br/>
|
||||
<strong>{_("Average GSD:")} </strong> {parseFloat(stats.gsd.toFixed(2)).toLocaleString()} cm<br/>
|
||||
</div> : ""}
|
||||
{stats && stats.area ?
|
||||
<div className="labels">
|
||||
<strong>{_("Area:")} </strong> {stats.area.toFixed(2)} m²<br/>
|
||||
<strong>{_("Area:")} </strong> {parseFloat(stats.area.toFixed(2)).toLocaleString()} m²<br/>
|
||||
</div> : ""}
|
||||
{stats && stats.pointcloud && stats.pointcloud.points ?
|
||||
<div className="labels">
|
||||
|
@ -580,6 +621,8 @@ class TaskListItem extends React.Component {
|
|||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
let statusIcon = statusCodes.icon(task.status);
|
||||
|
||||
// @param type {String} one of: ['neutral', 'done', 'error']
|
||||
const getStatusLabel = (text, type = 'neutral', progress = 100) => {
|
||||
|
@ -589,11 +632,10 @@ class TaskListItem extends React.Component {
|
|||
return (<div
|
||||
className={"status-label theme-border-primary " + type}
|
||||
style={{background: `linear-gradient(90deg, ${color} ${progress}%, rgba(255, 255, 255, 0) ${progress}%)`}}
|
||||
title={text}>{text}</div>);
|
||||
title={text}><i className={statusIcon}></i> {text}</div>);
|
||||
}
|
||||
|
||||
let statusLabel = "";
|
||||
let statusIcon = statusCodes.icon(task.status);
|
||||
let showEditLink = false;
|
||||
|
||||
if (task.last_error){
|
||||
|
@ -624,8 +666,55 @@ class TaskListItem extends React.Component {
|
|||
statusLabel = getStatusLabel(status, type, progress);
|
||||
}
|
||||
|
||||
const taskActions = [];
|
||||
const addTaskAction = (label, icon, onClick) => {
|
||||
taskActions.push(
|
||||
<li key={label}><a href="javascript:void(0)" onClick={onClick}><i className={icon}></i>{label}</a></li>
|
||||
);
|
||||
};
|
||||
|
||||
if ([statusCodes.QUEUED, statusCodes.RUNNING, null].indexOf(task.status) !== -1 &&
|
||||
(task.processing_node || imported) && this.props.hasPermission("change")){
|
||||
addTaskAction(_("Cancel"), "glyphicon glyphicon-remove-circle", this.genActionApiCall("cancel", {defaultError: _("Cannot cancel task.")}));
|
||||
}
|
||||
|
||||
// Ability to change options
|
||||
if (editable || (!task.processing_node)){
|
||||
taskActions.push(<li key="edit"><a href="javascript:void(0)" onClick={this.startEditing}><i className="glyphicon glyphicon-pencil"></i>{_("Edit")}</a></li>);
|
||||
}
|
||||
|
||||
if (editable){
|
||||
taskActions.push(
|
||||
<li key="move"><a href="javascript:void(0)" onClick={this.handleMoveTask}><i className="fa fa-arrows-alt"></i>{_("Move")}</a></li>,
|
||||
<li key="duplicate"><a href="javascript:void(0)" onClick={this.handleDuplicateTask}><i className="fa fa-copy"></i>{_("Duplicate")}</a></li>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (this.props.hasPermission("delete")){
|
||||
taskActions.push(
|
||||
<li key="sep" role="separator" className="divider"></li>,
|
||||
);
|
||||
|
||||
addTaskAction(_("Delete"), "fa fa-trash", this.genActionApiCall("remove", {
|
||||
confirm: _("All information related to this task, including images, maps and models will be deleted. Continue?"),
|
||||
defaultError: _("Cannot delete task.")
|
||||
}));
|
||||
}
|
||||
|
||||
let taskActionsIcon = "fa-ellipsis-h";
|
||||
if (actionLoading) taskActionsIcon = "fa-circle-notch fa-spin fa-fw";
|
||||
|
||||
return (
|
||||
<div className="task-list-item">
|
||||
{this.state.showMoveDialog ?
|
||||
<MoveTaskDialog
|
||||
task={task}
|
||||
ref={(domNode) => { this.moveTaskDialog = domNode; }}
|
||||
onHide={() => this.setState({showMoveDialog: false})}
|
||||
saveAction={this.moveTaskAction}
|
||||
/>
|
||||
: ""}
|
||||
<div className="row">
|
||||
<div className="col-sm-5 name">
|
||||
<i onClick={this.toggleExpanded} className={"clickable far " + (this.state.expanded ? "fa-minus-square" : " fa-plus-square")}></i> <a href="javascript:void(0);" onClick={this.toggleExpanded}>{name}</a>
|
||||
|
@ -642,9 +731,16 @@ class TaskListItem extends React.Component {
|
|||
: statusLabel}
|
||||
</div>
|
||||
<div className="col-sm-1 text-right">
|
||||
<div className="status-icon">
|
||||
<i className={statusIcon}></i>
|
||||
</div>
|
||||
{taskActions.length > 0 ?
|
||||
<div className="btn-group">
|
||||
<button disabled={disabled || actionLoading} className="btn task-actions btn-secondary btn-xs dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<i className={"fa " + taskActionsIcon}></i>
|
||||
</button>
|
||||
<ul className="dropdown-menu dropdown-menu-right">
|
||||
{taskActions}
|
||||
</ul>
|
||||
</div>
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
{expanded}
|
||||
|
|
|
@ -4,7 +4,7 @@ import TaskList from '../TaskList';
|
|||
|
||||
describe('<TaskList />', () => {
|
||||
it('renders without exploding', () => {
|
||||
const wrapper = shallow(<TaskList history={{}} source="tasklist.json" />);
|
||||
const wrapper = shallow(<TaskList history={{}} source="tasklist.json" hasPermission={() => true} />);
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
})
|
||||
});
|
|
@ -6,7 +6,7 @@ const taskMock = require('../../tests/utils/MockLoader').load("task.json");
|
|||
|
||||
describe('<TaskListItem />', () => {
|
||||
it('renders without exploding', () => {
|
||||
const wrapper = shallow(<TaskListItem history={createHistory()} data={taskMock} />);
|
||||
const wrapper = shallow(<TaskListItem history={createHistory()} data={taskMock} hasPermission={() => true} />);
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
})
|
||||
});
|
|
@ -55,6 +55,11 @@
|
|||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-danger .fa-trash{
|
||||
margin-right: 2px;
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
.dz-preview{
|
||||
display: none;
|
||||
|
|
|
@ -31,11 +31,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
.status-icon{
|
||||
margin-top: 3px;
|
||||
.fa-cog{
|
||||
width: auto;
|
||||
}
|
||||
.fa-cog{
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.status-label{
|
||||
|
|
|
@ -48,10 +48,14 @@ class TestApi(BootTestCase):
|
|||
client.login(username="testuser", password="test1234")
|
||||
res = client.get('/api/projects/')
|
||||
self.assertEqual(res.status_code, status.HTTP_200_OK)
|
||||
self.assertTrue(len(res.data["results"]) > 0)
|
||||
self.assertTrue(len(res.data) > 0)
|
||||
|
||||
res = client.get('/api/projects/?page=1')
|
||||
self.assertEqual(res.status_code, status.HTTP_200_OK)
|
||||
self.assertTrue(len(res.data['results']) > 0)
|
||||
|
||||
# Can sort
|
||||
res = client.get('/api/projects/?ordering=-created_at')
|
||||
res = client.get('/api/projects/?ordering=-created_at&page=1')
|
||||
last_project = Project.objects.filter(owner=user).latest('created_at')
|
||||
self.assertTrue(res.data["results"][0]['id'] == last_project.id)
|
||||
|
||||
|
@ -65,12 +69,12 @@ class TestApi(BootTestCase):
|
|||
self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# Can filter
|
||||
res = client.get('/api/projects/?name=999')
|
||||
res = client.get('/api/projects/?name=999&page=1')
|
||||
self.assertEqual(res.status_code, status.HTTP_200_OK)
|
||||
self.assertTrue(len(res.data["results"]) == 0)
|
||||
|
||||
# Cannot list somebody else's project without permission
|
||||
res = client.get('/api/projects/?id={}'.format(other_project.id))
|
||||
res = client.get('/api/projects/?id={}&page=1'.format(other_project.id))
|
||||
self.assertEqual(res.status_code, status.HTTP_200_OK)
|
||||
self.assertTrue(len(res.data["results"]) == 0)
|
||||
|
||||
|
@ -161,6 +165,21 @@ class TestApi(BootTestCase):
|
|||
res = client.get('/api/projects/{}/tasks/{}/'.format(project.id, other_task.id))
|
||||
self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# Cannot duplicate a project we have no access to
|
||||
res = client.post('/api/projects/{}/duplicate/'.format(other_project.id))
|
||||
self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# Can duplicate a project we have access to
|
||||
res = client.post('/api/projects/{}/duplicate/'.format(project.id))
|
||||
self.assertEqual(res.status_code, status.HTTP_200_OK)
|
||||
self.assertTrue(res.data.get('success'))
|
||||
new_project_id = res.data['project']['id']
|
||||
self.assertNotEqual(new_project_id, project.id)
|
||||
|
||||
# Tasks have been duplicated
|
||||
duplicated_project = Project.objects.get(pk=new_project_id)
|
||||
self.assertEqual(project.task_set.count(), duplicated_project.task_set.count())
|
||||
|
||||
# Cannot access task details for a task that doesn't exist
|
||||
res = client.get('/api/projects/{}/tasks/4004d1e9-ed2c-4983-8b93-fc7577ee6d89/'.format(project.id))
|
||||
self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
|
|
@ -292,6 +292,10 @@ class TestApiTask(BootTransactionTestCase):
|
|||
res = client.get("/api/projects/{}/tasks/{}/images/download/tiny_drone_image.jpg".format(other_project.id, other_task.id))
|
||||
self.assertTrue(res.status_code == status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# Cannot duplicate a task we have no access to
|
||||
res = client.post("/api/projects/{}/tasks/{}/duplicate/".format(other_project.id, other_task.id))
|
||||
self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# Cannot export orthophoto
|
||||
res = client.post("/api/projects/{}/tasks/{}/orthophoto/export".format(project.id, task.id), {
|
||||
'formula': 'NDVI',
|
||||
|
@ -898,6 +902,21 @@ class TestApiTask(BootTransactionTestCase):
|
|||
self.assertFalse('orthophoto_tiles.zip' in res.data['available_assets'])
|
||||
self.assertTrue('textured_model.zip' in res.data['available_assets'])
|
||||
|
||||
# Can duplicate a task
|
||||
res = client.post("/api/projects/{}/tasks/{}/duplicate/".format(project.id, task.id))
|
||||
self.assertTrue(res.status_code, status.HTTP_200_OK)
|
||||
self.assertTrue(res.data['success'])
|
||||
new_task_id = res.data['task']['id']
|
||||
self.assertNotEqual(res.data['task']['id'], task.id)
|
||||
|
||||
new_task = Task.objects.get(pk=new_task_id)
|
||||
|
||||
# New task has same number of image uploads
|
||||
self.assertEqual(task.imageupload_set.count(), new_task.imageupload_set.count())
|
||||
|
||||
# Directories have been created
|
||||
self.assertTrue(os.path.exists(new_task.task_path()))
|
||||
|
||||
image1.close()
|
||||
image2.close()
|
||||
multispec_image.close()
|
||||
|
|
|
@ -32,6 +32,7 @@ services:
|
|||
- WO_DEBUG
|
||||
- WO_BROKER
|
||||
- WO_DEV
|
||||
- WO_DEV_WATCH_PLUGINS
|
||||
restart: unless-stopped
|
||||
oom_score_adj: 0
|
||||
broker:
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "WebODM",
|
||||
"version": "1.9.4",
|
||||
"version": "1.9.5",
|
||||
"description": "User-friendly, extendable application and API for processing aerial imagery.",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
|
|
@ -82,6 +82,10 @@ case $key in
|
|||
export WO_DEBUG=YES
|
||||
shift # past argument
|
||||
;;
|
||||
--dev-watch-plugins)
|
||||
export WO_DEV_WATCH_PLUGINS=YES
|
||||
shift # past argument
|
||||
;;
|
||||
--dev)
|
||||
export WO_DEBUG=YES
|
||||
export WO_DEV=YES
|
||||
|
@ -148,6 +152,7 @@ usage(){
|
|||
echo " --ssl-insecure-port-redirect <port> Insecure port number to redirect from when SSL is enabled (default: $DEFAULT_SSL_INSECURE_PORT_REDIRECT)"
|
||||
echo " --debug Enable debug for development environments (default: disabled)"
|
||||
echo " --dev Enable development mode. In development mode you can make modifications to WebODM source files and changes will be reflected live. (default: disabled)"
|
||||
echo " --dev-watch-plugins Automatically build plugins while in dev mode. (default: disabled)"
|
||||
echo " --broker Set the URL used to connect to the celery broker (default: $DEFAULT_BROKER)"
|
||||
echo " --detached Run WebODM in detached mode. This means WebODM will run in the background, without blocking the terminal (default: disabled)"
|
||||
exit
|
||||
|
|
|
@ -52,6 +52,7 @@ WORKER_RUNNING = sys.argv[2:3] == ["worker"]
|
|||
# SECURITY WARNING: don't run with debug turned on a public facing server!
|
||||
DEBUG = os.environ.get('WO_DEBUG', 'YES') == 'YES' or TESTING
|
||||
DEV = os.environ.get('WO_DEV', 'NO') == 'YES' and not TESTING
|
||||
DEV_WATCH_PLUGINS = DEV and os.environ.get('WO_DEV_WATCH_PLUGINS', 'NO') == 'YES'
|
||||
SESSION_COOKIE_SECURE = CSRF_COOKIE_SECURE = os.environ.get('WO_SSL', 'NO') == 'YES'
|
||||
INTERNAL_IPS = ['127.0.0.1']
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue