Add duplicate task functionality, post process pipeline step

pull/1031/head
Piero Toffanin 2021-08-05 12:10:30 -04:00
rodzic dee1e0d880
commit 8fa2cc9643
5 zmienionych plików z 94 dodań i 8 usunięć

Wyświetl plik

@ -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', ))

Wyświetl plik

@ -373,6 +373,41 @@ class Task(models.Model):
else:
return {}
def duplicate(self):
try:
with transaction.atomic():
task = Task.objects.get(pk=self.pk)
task.pk = None
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()
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())
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

Wyświetl plik

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

Wyświetl plik

@ -97,6 +97,7 @@ class TaskList extends React.Component {
refreshInterval={3000}
onDelete={this.deleteTask}
onMove={this.moveTask}
onDuplicate={this.refresh}
hasPermission={this.props.hasPermission}
history={this.props.history} />
))}

Wyświetl plik

@ -22,6 +22,7 @@ class TaskListItem extends React.Component {
refreshInterval: PropTypes.number, // how often to refresh info
onDelete: PropTypes.func,
onMove: PropTypes.func,
onDuplicate: PropTypes.func,
hasPermission: PropTypes.func
}
@ -41,7 +42,8 @@ class TaskListItem extends React.Component {
friendlyTaskError: "",
pluginActionButtons: [],
view: "basic",
showMoveDialog: false
showMoveDialog: false,
actionLoading: false,
}
for (let k in props.data){
@ -198,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
});
}
})
@ -211,6 +214,9 @@ class TaskListItem extends React.Component {
actionError: options.defaultError || _("Cannot complete operation."),
actionButtonsDisabled: false
});
})
.always(() => {
if (options.always !== undefined) options.always();
});
}
@ -279,6 +285,19 @@ class TaskListItem extends React.Component {
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;
@ -398,6 +417,7 @@ class TaskListItem extends React.Component {
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){
@ -439,6 +459,11 @@ class TaskListItem extends React.Component {
});
}
if ([statusCodes.QUEUED, statusCodes.RUNNING, null].indexOf(task.status) !== -1 &&
(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") &&
@ -533,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&sup2;<br/>
<strong>{_("Area:")} </strong> {parseFloat(stats.area.toFixed(2)).toLocaleString()} m&sup2;<br/>
</div> : ""}
{stats && stats.pointcloud && stats.pointcloud.points ?
<div className="labels">
@ -661,7 +686,7 @@ class TaskListItem extends React.Component {
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)"><i className="fa fa-copy"></i>{_("Duplicate")}</a></li>
<li key="duplicate"><a href="javascript:void(0)" onClick={this.handleDuplicateTask}><i className="fa fa-copy"></i>{_("Duplicate")}</a></li>
);
}
@ -677,6 +702,9 @@ class TaskListItem extends React.Component {
}));
}
let taskActionsIcon = "fa-ellipsis-h";
if (actionLoading) taskActionsIcon = "fa-circle-notch fa-spin fa-fw";
return (
<div className="task-list-item">
{this.state.showMoveDialog ?
@ -705,8 +733,8 @@ class TaskListItem extends React.Component {
<div className="col-sm-1 text-right">
{taskActions.length > 0 ?
<div className="btn-group">
<button disabled={disabled} className="btn task-actions btn-secondary btn-xs dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i className="fa fa-ellipsis-h"></i>
<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}