kopia lustrzana https://github.com/OpenDroneMap/WebODM
Restart, cancel and delete actions working, UI sync and performance improvements
rodzic
31e46bbfb9
commit
145103ce03
|
@ -1,5 +1,5 @@
|
|||
from django.contrib.auth.models import User
|
||||
from rest_framework import serializers, viewsets
|
||||
from rest_framework import serializers, viewsets, filters
|
||||
|
||||
from app import models
|
||||
from .tasks import TaskIDsSerializer
|
||||
|
@ -26,3 +26,4 @@ class ProjectViewSet(viewsets.ModelViewSet):
|
|||
filter_fields = ('id', 'owner', 'name')
|
||||
serializer_class = ProjectSerializer
|
||||
queryset = models.Project.objects.all()
|
||||
ordering_fields = '__all__'
|
|
@ -50,15 +50,14 @@ class TaskViewSet(viewsets.ViewSet):
|
|||
raise exceptions.NotFound()
|
||||
return project
|
||||
|
||||
@detail_route(methods=['post'])
|
||||
def cancel(self, request, pk=None, project_pk=None):
|
||||
def set_pending_action(self, pending_action, request, pk=None, project_pk=None):
|
||||
self.get_and_check_project(request, project_pk, ('change_project',))
|
||||
try:
|
||||
task = self.queryset.get(pk=pk, project=project_pk)
|
||||
except ObjectDoesNotExist:
|
||||
raise exceptions.NotFound()
|
||||
|
||||
task.pending_action = task.PendingActions.CANCEL
|
||||
task.pending_action = pending_action
|
||||
task.last_error = None
|
||||
task.save()
|
||||
|
||||
|
@ -67,6 +66,19 @@ class TaskViewSet(viewsets.ViewSet):
|
|||
|
||||
return Response({'success': True})
|
||||
|
||||
@detail_route(methods=['post'])
|
||||
def cancel(self, *args, **kwargs):
|
||||
return self.set_pending_action(models.Task.PendingActions.CANCEL, *args, **kwargs)
|
||||
|
||||
@detail_route(methods=['post'])
|
||||
def restart(self, *args, **kwargs):
|
||||
return self.set_pending_action(models.Task.PendingActions.RESTART, *args, **kwargs)
|
||||
|
||||
@detail_route(methods=['post'])
|
||||
def remove(self, *args, **kwargs):
|
||||
# TODO: this should check for delete_project perms
|
||||
return self.set_pending_action(models.Task.PendingActions.REMOVE, *args, **kwargs)
|
||||
|
||||
@detail_route(methods=['get'])
|
||||
def output(self, request, pk=None, project_pk=None):
|
||||
"""
|
||||
|
|
|
@ -78,7 +78,8 @@ def validate_task_options(value):
|
|||
class Task(models.Model):
|
||||
class PendingActions:
|
||||
CANCEL = 1
|
||||
DELETE = 2
|
||||
REMOVE = 2
|
||||
RESTART = 3
|
||||
|
||||
STATUS_CODES = (
|
||||
(status_codes.QUEUED, 'QUEUED'),
|
||||
|
@ -90,7 +91,8 @@ class Task(models.Model):
|
|||
|
||||
PENDING_ACTIONS = (
|
||||
(PendingActions.CANCEL, 'CANCEL'),
|
||||
(PendingActions.DELETE, 'DELETE'),
|
||||
(PendingActions.REMOVE, 'REMOVE'),
|
||||
(PendingActions.RESTART, 'RESTART'),
|
||||
)
|
||||
|
||||
uuid = models.CharField(max_length=255, db_index=True, default='', blank=True, help_text="Identifier of the task (as returned by OpenDroneMap's REST API)")
|
||||
|
@ -176,11 +178,62 @@ class Task(models.Model):
|
|||
if self.processing_node and self.uuid:
|
||||
self.processing_node.cancel_task(self.uuid)
|
||||
self.pending_action = None
|
||||
self.save()
|
||||
else:
|
||||
raise ProcessingException("Cannot cancel a task that has no processing node or UUID assigned")
|
||||
raise ProcessingException("Cannot cancel a task that has no processing node or UUID")
|
||||
|
||||
elif self.pending_action == self.PendingActions.RESTART:
|
||||
logger.info("Restarting task {}".format(self))
|
||||
if self.processing_node and self.uuid:
|
||||
|
||||
# Check if the UUID is still valid, as processing nodes purge
|
||||
# results after a set amount of time, the UUID might have eliminated.
|
||||
try:
|
||||
info = self.processing_node.get_task_info(self.uuid)
|
||||
uuid_still_exists = info['uuid'] == self.uuid
|
||||
except ProcessingException:
|
||||
uuid_still_exists = False
|
||||
|
||||
if uuid_still_exists:
|
||||
# Good to go
|
||||
self.processing_node.restart_task(self.uuid)
|
||||
else:
|
||||
# Task has been purged (or processing node is offline)
|
||||
# TODO: what if processing node went offline?
|
||||
|
||||
# Process this as a new task
|
||||
# Removing its UUID will cause the scheduler
|
||||
# to process this the next tick
|
||||
self.uuid = None
|
||||
|
||||
self.console_output = ""
|
||||
self.processing_time = -1
|
||||
self.status = None
|
||||
self.last_error = None
|
||||
self.pending_action = None
|
||||
self.save()
|
||||
else:
|
||||
raise ProcessingException("Cannot restart a task that has no processing node or UUID")
|
||||
|
||||
elif self.pending_action == self.PendingActions.REMOVE:
|
||||
logger.info("Removing task {}".format(self))
|
||||
if self.processing_node and self.uuid:
|
||||
# Attempt to delete the resources on the processing node
|
||||
# We don't care if this fails, as resources on processing nodes
|
||||
# Are expected to be purged on their own after a set amount of time anyway
|
||||
try:
|
||||
self.processing_node.remove_task(self.uuid)
|
||||
except ProcessingException:
|
||||
pass
|
||||
|
||||
# What's more important is that we delete our task properly here
|
||||
self.delete()
|
||||
|
||||
# Stop right here!
|
||||
return
|
||||
|
||||
except ProcessingException as e:
|
||||
self.last_error = e.message
|
||||
finally:
|
||||
self.save()
|
||||
|
||||
|
||||
|
@ -224,6 +277,7 @@ class Task(models.Model):
|
|||
self.status = status_codes.FAILED
|
||||
self.save()
|
||||
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
('view_task', 'Can view task'),
|
||||
|
|
|
@ -75,6 +75,9 @@ def process_pending_tasks():
|
|||
|
||||
def process(task):
|
||||
task.process()
|
||||
|
||||
# Might have been deleted
|
||||
if task.pk is not None:
|
||||
task.processing_lock = False
|
||||
task.save()
|
||||
|
||||
|
|
|
@ -26,14 +26,14 @@ class Console extends React.Component {
|
|||
|
||||
componentDidMount(){
|
||||
this.checkAutoscroll();
|
||||
this.setupDynamicSource();
|
||||
}
|
||||
|
||||
// Dynamic source?
|
||||
setupDynamicSource(){
|
||||
if (this.props.source !== undefined){
|
||||
let currentLineNumber = 0;
|
||||
|
||||
const updateFromSource = () => {
|
||||
let sourceUrl = typeof this.props.source === 'function' ?
|
||||
this.props.source(currentLineNumber) :
|
||||
this.props.source(this.state.lines.length) :
|
||||
this.props.source;
|
||||
|
||||
// Fetch
|
||||
|
@ -41,7 +41,6 @@ class Console extends React.Component {
|
|||
if (text !== ""){
|
||||
let lines = text.split("\n");
|
||||
this.addLines(lines);
|
||||
currentLineNumber += (lines.length - 1);
|
||||
}
|
||||
})
|
||||
.always((_, textStatus) => {
|
||||
|
@ -56,11 +55,21 @@ class Console extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
componentWillUnmount(){
|
||||
clear(){
|
||||
this.tearDownDynamicSource();
|
||||
this.setState({lines: []});
|
||||
this.setupDynamicSource();
|
||||
}
|
||||
|
||||
tearDownDynamicSource(){
|
||||
if (this.sourceTimeout) clearTimeout(this.sourceTimeout);
|
||||
if (this.sourceRequest) this.sourceRequest.abort();
|
||||
}
|
||||
|
||||
componentWillUnmount(){
|
||||
this.tearDownDynamicSource();
|
||||
}
|
||||
|
||||
setRef(domNode){
|
||||
if (domNode != null){
|
||||
this.$console = $(domNode);
|
||||
|
@ -91,7 +100,6 @@ class Console extends React.Component {
|
|||
|
||||
render() {
|
||||
const prettyLine = (line) => {
|
||||
// TODO Escape
|
||||
return {__html: prettyPrintOne(Utils.escapeHtml(line), this.props.lang, this.props.lines)};
|
||||
}
|
||||
let i = 0;
|
||||
|
|
|
@ -5,7 +5,7 @@ import ProjectList from './components/ProjectList';
|
|||
class Dashboard extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<ProjectList source="/api/projects/"/>
|
||||
<ProjectList source="/api/projects/?ordering=-created_at"/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,23 @@
|
|||
const CANCEL = 1,
|
||||
DELETE = 2;
|
||||
REMOVE = 2,
|
||||
RESTART = 3;
|
||||
|
||||
let pendingActions = {
|
||||
[CANCEL]: {
|
||||
descr: "Canceling..."
|
||||
},
|
||||
[DELETE]: {
|
||||
[REMOVE]: {
|
||||
descr: "Deleting..."
|
||||
},
|
||||
[RESTART]: {
|
||||
descr: "Restarting..."
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
CANCEL: CANCEL,
|
||||
DELETE: DELETE,
|
||||
REMOVE: REMOVE,
|
||||
RESTART: RESTART,
|
||||
|
||||
description: function(pendingAction) {
|
||||
if (pendingActions[pendingAction]) return pendingActions[pendingAction].descr;
|
||||
|
|
|
@ -15,7 +15,7 @@ let statusCodes = {
|
|||
},
|
||||
[FAILED]: {
|
||||
descr: "Failed",
|
||||
icon: "fa fa-remove"
|
||||
icon: "fa fa-frown-o"
|
||||
},
|
||||
[COMPLETED]: {
|
||||
descr: "Completed",
|
||||
|
|
|
@ -167,7 +167,6 @@ class ProjectListItem extends React.Component {
|
|||
this.setState({
|
||||
showTaskList: !this.state.showTaskList
|
||||
});
|
||||
console.log(this.props);
|
||||
}
|
||||
|
||||
closeUploadError(){
|
||||
|
@ -264,7 +263,7 @@ class ProjectListItem extends React.Component {
|
|||
<span>Updating task information... <i className="fa fa-refresh fa-spin fa-fw"></i></span>
|
||||
: ""}
|
||||
|
||||
{this.state.showTaskList ? <TaskList ref={this.setRef("taskList")} source={`/api/projects/${this.props.data.id}/tasks/?ordering=-id`}/> : ""}
|
||||
{this.state.showTaskList ? <TaskList ref={this.setRef("taskList")} source={`/api/projects/${this.props.data.id}/tasks/?ordering=-created_at`}/> : ""}
|
||||
|
||||
</div>
|
||||
</li>
|
||||
|
|
|
@ -13,6 +13,7 @@ class TaskList extends React.Component {
|
|||
};
|
||||
|
||||
this.refresh = this.refresh.bind(this);
|
||||
this.deleteTask = this.deleteTask.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount(){
|
||||
|
@ -46,6 +47,12 @@ class TaskList extends React.Component {
|
|||
this.taskListRequest.abort();
|
||||
}
|
||||
|
||||
deleteTask(id){
|
||||
this.setState({
|
||||
tasks: this.state.tasks.filter(t => t.id !== id)
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
let message = "";
|
||||
if (this.state.loading){
|
||||
|
@ -61,7 +68,7 @@ class TaskList extends React.Component {
|
|||
{message}
|
||||
|
||||
{this.state.tasks.map(task => (
|
||||
<TaskListItem data={task} key={task.id} refreshInterval={3000} />
|
||||
<TaskListItem data={task} key={task.id} refreshInterval={3000} onDelete={this.deleteTask} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -27,9 +27,9 @@ class TaskListItem extends React.Component {
|
|||
|
||||
shouldRefresh(){
|
||||
// If a task is completed, or failed, etc. we don't expect it to change
|
||||
return (this.state.task.status === statusCodes.QUEUED ||
|
||||
this.state.task.status === statusCodes.RUNNING ||
|
||||
(!this.state.task.uuid && this.state.task.processing_node && !this.state.task.last_error));
|
||||
return (([statusCodes.QUEUED, statusCodes.RUNNING, null].indexOf(this.state.task.status) !== -1 && this.state.task.processing_node) ||
|
||||
(!this.state.task.uuid && this.state.task.processing_node && !this.state.task.last_error) ||
|
||||
this.state.task.pending_action !== null);
|
||||
}
|
||||
|
||||
loadTimer(startTime){
|
||||
|
@ -65,6 +65,7 @@ class TaskListItem extends React.Component {
|
|||
// Update timer if we switched to running
|
||||
if (oldStatus !== this.state.task.status){
|
||||
if (this.state.task.status === statusCodes.RUNNING){
|
||||
this.console.clear();
|
||||
this.loadTimer(this.state.task.processing_time);
|
||||
}else{
|
||||
this.setState({time: this.state.task.processing_time});
|
||||
|
@ -74,18 +75,21 @@ class TaskListItem extends React.Component {
|
|||
}else{
|
||||
console.warn("Cannot refresh task: " + json);
|
||||
}
|
||||
})
|
||||
.always((_, textStatus) => {
|
||||
if (textStatus !== "abort"){
|
||||
|
||||
if (this.shouldRefresh()) this.refreshTimeout = setTimeout(() => this.refresh(), this.props.refreshInterval || 3000);
|
||||
})
|
||||
.fail(( _, __, errorThrown) => {
|
||||
if (errorThrown === "Not Found"){ // Don't translate this one
|
||||
// Assume this has been deleted
|
||||
if (this.props.onDelete) this.props.onDelete(this.state.task.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount(){
|
||||
this.unloadTimer();
|
||||
if (this.refreshTimeout) clearTimeout(this.refreshTimeout);
|
||||
if (this.refreshRequest) this.refreshRequest.abort();
|
||||
if (this.refreshTimeout) clearTimeout(this.refreshTimeout);
|
||||
}
|
||||
|
||||
toggleExpanded(){
|
||||
|
@ -118,8 +122,9 @@ class TaskListItem extends React.Component {
|
|||
return [pad(h), pad(m), pad(s)].join(':');
|
||||
}
|
||||
|
||||
genActionApiCall(action){
|
||||
genActionApiCall(action, options = {}){
|
||||
return () => {
|
||||
const doAction = () => {
|
||||
this.setState({actionButtonsDisabled: true});
|
||||
|
||||
$.post(`/api/projects/${this.state.task.project}/tasks/${this.state.task.id}/${action}/`,
|
||||
|
@ -129,6 +134,7 @@ class TaskListItem extends React.Component {
|
|||
).done(json => {
|
||||
if (json.success){
|
||||
this.refresh();
|
||||
if (options.success !== undefined) options.success();
|
||||
}else{
|
||||
this.setState({
|
||||
actionError: json.error,
|
||||
|
@ -142,14 +148,32 @@ class TaskListItem extends React.Component {
|
|||
actionButtonsDisabled: false
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (options.confirm){
|
||||
if (window.confirm(options.confirm)){
|
||||
doAction();
|
||||
}
|
||||
}else{
|
||||
doAction();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
optionsToList(options){
|
||||
if (!Array.isArray(options)) return "";
|
||||
else if (options.length === 0) return "Default";
|
||||
else {
|
||||
return options.map(opt => `${opt.name}: ${opt.value}`).join(", ");
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let name = this.state.task.name !== null ? this.state.task.name : `Task #${this.state.task.id}`;
|
||||
|
||||
let status = statusCodes.description(this.state.task.status);
|
||||
if (status === "") status = "Uploading images";
|
||||
if (!this.state.task.processing_node) status = "";
|
||||
if (this.state.task.pending_action !== null) status = pendingActions.description(this.state.task.pending_action);
|
||||
|
||||
let expanded = "";
|
||||
|
@ -161,19 +185,30 @@ class TaskListItem extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
if ([statusCodes.QUEUED, statusCodes.RUNNING, null].indexOf(this.state.task.status) !== -1){
|
||||
if ([statusCodes.QUEUED, statusCodes.RUNNING, null].indexOf(this.state.task.status) !== -1 &&
|
||||
this.state.task.processing_node){
|
||||
addActionButton("Cancel", "btn-primary", "glyphicon glyphicon-remove-circle", this.genActionApiCall("cancel"));
|
||||
}
|
||||
|
||||
// addActionButton("Restart", "btn-primary", "glyphicon glyphicon-play", genActionApiCall("cancel"));
|
||||
if ([statusCodes.FAILED, statusCodes.COMPLETED, statusCodes.CANCELED].indexOf(this.state.task.status) !== -1 &&
|
||||
this.state.task.processing_node){
|
||||
addActionButton("Restart", "btn-primary", "glyphicon glyphicon-remove-circle", this.genActionApiCall("restart", {
|
||||
success: () => {
|
||||
this.console.clear();
|
||||
this.setState({time: -1});
|
||||
}
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
// TODO: ability to change options
|
||||
// addActionButton("Edit", "btn-primary", "glyphicon glyphicon-pencil", () => {
|
||||
// console.log("edit call");
|
||||
// });
|
||||
|
||||
// addActionButton("Delete", "btn-danger", "glyphicon glyphicon-trash", () => {
|
||||
// console.log("Delete call");
|
||||
// });
|
||||
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?"
|
||||
}));
|
||||
|
||||
actionButtons = (<div className="action-buttons">
|
||||
{actionButtons.map(button => {
|
||||
|
@ -195,6 +230,9 @@ class TaskListItem extends React.Component {
|
|||
</div>
|
||||
<div className="labels">
|
||||
<strong>Status: </strong> {status}<br/>
|
||||
</div>
|
||||
<div className="labels">
|
||||
<strong>Options: </strong> {this.optionsToList(this.state.task.options)}<br/>
|
||||
</div>
|
||||
{/* TODO: List of images? */}
|
||||
</div>
|
||||
|
@ -203,7 +241,9 @@ class TaskListItem extends React.Component {
|
|||
source={this.consoleOutputUrl}
|
||||
refreshInterval={this.shouldRefresh() ? 3000 : undefined}
|
||||
autoscroll={true}
|
||||
height={200} />
|
||||
height={200}
|
||||
ref={domNode => this.console = domNode}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
|
|
|
@ -16,7 +16,6 @@ class UploadProgressBar extends React.Component {
|
|||
let percentage = (this.props.progress !== undefined ?
|
||||
this.props.progress :
|
||||
0).toFixed(2);
|
||||
|
||||
let bytes = this.props.totalBytesSent !== undefined && this.props.totalBytes !== undefined ?
|
||||
`, remaining to upload: ${this.bytesToSize(this.props.totalBytes - this.props.totalBytesSent)}` :
|
||||
"";
|
||||
|
|
|
@ -586,11 +586,13 @@
|
|||
eventName = _ref1[_i];
|
||||
this.on(eventName, this.options[eventName]);
|
||||
}
|
||||
|
||||
this.on("uploadprogress", (function(_this) {
|
||||
return function() {
|
||||
return _this.updateTotalUploadProgress();
|
||||
};
|
||||
})(this));
|
||||
|
||||
this.on("removedfile", (function(_this) {
|
||||
return function() {
|
||||
return _this.updateTotalUploadProgress();
|
||||
|
@ -703,8 +705,9 @@
|
|||
_ref = this.getActiveFiles();
|
||||
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
|
||||
file = _ref[_i];
|
||||
totalBytesSent += file.upload.bytesSent;
|
||||
totalBytes += file.upload.total;
|
||||
totalBytesSent = file.upload.bytesSent;
|
||||
totalBytes = file.upload.total;
|
||||
break;
|
||||
}
|
||||
totalUploadProgress = 100 * totalBytesSent / totalBytes;
|
||||
} else {
|
||||
|
@ -1288,6 +1291,7 @@
|
|||
for (_l = 0, _len3 = files.length; _l < _len3; _l++) {
|
||||
file = files[_l];
|
||||
_results.push(_this.emit("uploadprogress", file, progress, file.upload.bytesSent));
|
||||
break;
|
||||
}
|
||||
return _results;
|
||||
};
|
||||
|
|
|
@ -37,6 +37,11 @@ class TestApi(BootTestCase):
|
|||
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')
|
||||
last_project = Project.objects.filter(owner=user).latest('created_at')
|
||||
self.assertTrue(res.data["results"][0]['id'] == last_project.id)
|
||||
|
||||
res = client.get('/api/projects/{}/'.format(project.id))
|
||||
self.assertEqual(res.status_code, status.HTTP_200_OK)
|
||||
|
||||
|
@ -77,11 +82,11 @@ class TestApi(BootTestCase):
|
|||
self.assertTrue(len(res.data) == 2)
|
||||
|
||||
# Can sort
|
||||
res = client.get('/api/projects/{}/tasks/?ordering=id'.format(project.id))
|
||||
res = client.get('/api/projects/{}/tasks/?ordering=created_at'.format(project.id))
|
||||
self.assertTrue(res.data[0]['id'] == task.id)
|
||||
self.assertTrue(res.data[1]['id'] == task2.id)
|
||||
|
||||
res = client.get('/api/projects/{}/tasks/?ordering=-id'.format(project.id))
|
||||
res = client.get('/api/projects/{}/tasks/?ordering=-created_at'.format(project.id))
|
||||
self.assertTrue(res.data[0]['id'] == task2.id)
|
||||
self.assertTrue(res.data[1]['id'] == task.id)
|
||||
|
||||
|
@ -159,6 +164,8 @@ class TestApi(BootTestCase):
|
|||
res = client.post('/api/projects/{}/tasks/{}/cancel/'.format(other_project.id, other_task.id))
|
||||
self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# TODO: test restart and delete operations
|
||||
|
||||
|
||||
def test_processingnodes(self):
|
||||
client = APIClient()
|
||||
|
|
|
@ -34,6 +34,11 @@ class ApiClient:
|
|||
def task_cancel(self, uuid):
|
||||
return requests.post(self.url('/task/cancel'), data={'uuid': uuid}).json()
|
||||
|
||||
def task_remove(self, uuid):
|
||||
return requests.post(self.url('/task/remove'), data={'uuid': uuid}).json()
|
||||
|
||||
def task_restart(self, uuid):
|
||||
return requests.post(self.url('/task/restart'), data={'uuid': uuid}).json()
|
||||
|
||||
def new_task(self, images, name=None, options=[]):
|
||||
"""
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit fdc55291525c03dbdf922724f1a4af8d1a89d2af
|
||||
Subproject commit 038940860d043439b184c4fbd990f8a71b60c324
|
|
@ -80,10 +80,12 @@ class ProcessingNode(models.Model):
|
|||
"""
|
||||
api_client = self.api_client()
|
||||
result = api_client.task_info(uuid)
|
||||
if result['uuid']:
|
||||
if isinstance(result, dict) and 'uuid' in result:
|
||||
return result
|
||||
elif result['error']:
|
||||
elif isinstance(result, dict) and 'error' in result:
|
||||
raise ProcessingException(result['error'])
|
||||
else:
|
||||
raise ProcessingException("Unknown result from task info: {}".format(result))
|
||||
|
||||
def get_task_console_output(self, uuid, line):
|
||||
"""
|
||||
|
@ -106,6 +108,20 @@ class ProcessingNode(models.Model):
|
|||
api_client = self.api_client()
|
||||
return self.handle_generic_post_response(api_client.task_cancel(uuid))
|
||||
|
||||
def remove_task(self, uuid):
|
||||
"""
|
||||
Removes a task and deletes all of its assets
|
||||
"""
|
||||
api_client = self.api_client()
|
||||
return self.handle_generic_post_response(api_client.task_remove(uuid))
|
||||
|
||||
def restart_task(self, uuid):
|
||||
"""
|
||||
Restarts a task that was previously canceled or that had failed to process
|
||||
"""
|
||||
api_client = self.api_client()
|
||||
return self.handle_generic_post_response(api_client.task_restart(uuid))
|
||||
|
||||
@staticmethod
|
||||
def handle_generic_post_response(result):
|
||||
"""
|
||||
|
|
|
@ -97,7 +97,20 @@ class TestClientApi(TestCase):
|
|||
|
||||
self.assertRaises(ProcessingException, online_node.get_task_console_output, "wrong-uuid", 0)
|
||||
|
||||
# Can restart task
|
||||
self.assertTrue(online_node.restart_task(uuid))
|
||||
self.assertRaises(ProcessingException, online_node.restart_task, "wrong-uuid")
|
||||
|
||||
# Can cancel task
|
||||
self.assertTrue(online_node.cancel_task(uuid))
|
||||
self.assertRaises(ProcessingException, online_node.cancel_task, "wrong-uuid")
|
||||
|
||||
# Can delete task
|
||||
self.assertTrue(online_node.remove_task(uuid))
|
||||
self.assertRaises(ProcessingException, online_node.remove_task, "wrong-uuid")
|
||||
|
||||
# Cannot delete task again
|
||||
self.assertRaises(ProcessingException, online_node.remove_task, uuid)
|
||||
|
||||
# Task has been deleted
|
||||
self.assertRaises(ProcessingException, online_node.get_task_info, uuid)
|
|
@ -212,6 +212,7 @@ REST_FRAMEWORK = {
|
|||
'DEFAULT_FILTER_BACKENDS': [
|
||||
'rest_framework.filters.DjangoObjectPermissionsFilter',
|
||||
'rest_framework.filters.DjangoFilterBackend',
|
||||
'rest_framework.filters.OrderingFilter',
|
||||
],
|
||||
'PAGE_SIZE': 10,
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue