Restart, cancel and delete actions working, UI sync and performance improvements

pull/43/head
Piero Toffanin 2016-11-04 14:19:18 -04:00
rodzic 31e46bbfb9
commit 145103ce03
19 zmienionych plików z 243 dodań i 69 usunięć

Wyświetl plik

@ -1,5 +1,5 @@
from django.contrib.auth.models import User 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 app import models
from .tasks import TaskIDsSerializer from .tasks import TaskIDsSerializer
@ -26,3 +26,4 @@ class ProjectViewSet(viewsets.ModelViewSet):
filter_fields = ('id', 'owner', 'name') filter_fields = ('id', 'owner', 'name')
serializer_class = ProjectSerializer serializer_class = ProjectSerializer
queryset = models.Project.objects.all() queryset = models.Project.objects.all()
ordering_fields = '__all__'

Wyświetl plik

@ -50,15 +50,14 @@ class TaskViewSet(viewsets.ViewSet):
raise exceptions.NotFound() raise exceptions.NotFound()
return project return project
@detail_route(methods=['post']) def set_pending_action(self, pending_action, request, pk=None, project_pk=None):
def cancel(self, request, pk=None, project_pk=None):
self.get_and_check_project(request, project_pk, ('change_project',)) self.get_and_check_project(request, project_pk, ('change_project',))
try: try:
task = self.queryset.get(pk=pk, project=project_pk) task = self.queryset.get(pk=pk, project=project_pk)
except ObjectDoesNotExist: except ObjectDoesNotExist:
raise exceptions.NotFound() raise exceptions.NotFound()
task.pending_action = task.PendingActions.CANCEL task.pending_action = pending_action
task.last_error = None task.last_error = None
task.save() task.save()
@ -67,6 +66,19 @@ class TaskViewSet(viewsets.ViewSet):
return Response({'success': True}) 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']) @detail_route(methods=['get'])
def output(self, request, pk=None, project_pk=None): def output(self, request, pk=None, project_pk=None):
""" """

Wyświetl plik

@ -78,7 +78,8 @@ def validate_task_options(value):
class Task(models.Model): class Task(models.Model):
class PendingActions: class PendingActions:
CANCEL = 1 CANCEL = 1
DELETE = 2 REMOVE = 2
RESTART = 3
STATUS_CODES = ( STATUS_CODES = (
(status_codes.QUEUED, 'QUEUED'), (status_codes.QUEUED, 'QUEUED'),
@ -90,7 +91,8 @@ class Task(models.Model):
PENDING_ACTIONS = ( PENDING_ACTIONS = (
(PendingActions.CANCEL, 'CANCEL'), (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)") 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: if self.processing_node and self.uuid:
self.processing_node.cancel_task(self.uuid) self.processing_node.cancel_task(self.uuid)
self.pending_action = None self.pending_action = None
self.save()
else: 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: except ProcessingException as e:
self.last_error = e.message self.last_error = e.message
finally:
self.save() self.save()
@ -224,6 +277,7 @@ class Task(models.Model):
self.status = status_codes.FAILED self.status = status_codes.FAILED
self.save() self.save()
class Meta: class Meta:
permissions = ( permissions = (
('view_task', 'Can view task'), ('view_task', 'Can view task'),

Wyświetl plik

@ -75,8 +75,11 @@ def process_pending_tasks():
def process(task): def process(task):
task.process() task.process()
task.processing_lock = False
task.save() # Might have been deleted
if task.pk is not None:
task.processing_lock = False
task.save()
if tasks.count() > 0: if tasks.count() > 0:
pool = ThreadPool(tasks.count()) pool = ThreadPool(tasks.count())

Wyświetl plik

@ -26,14 +26,14 @@ class Console extends React.Component {
componentDidMount(){ componentDidMount(){
this.checkAutoscroll(); this.checkAutoscroll();
this.setupDynamicSource();
}
// Dynamic source? setupDynamicSource(){
if (this.props.source !== undefined){ if (this.props.source !== undefined){
let currentLineNumber = 0;
const updateFromSource = () => { const updateFromSource = () => {
let sourceUrl = typeof this.props.source === 'function' ? let sourceUrl = typeof this.props.source === 'function' ?
this.props.source(currentLineNumber) : this.props.source(this.state.lines.length) :
this.props.source; this.props.source;
// Fetch // Fetch
@ -41,7 +41,6 @@ class Console extends React.Component {
if (text !== ""){ if (text !== ""){
let lines = text.split("\n"); let lines = text.split("\n");
this.addLines(lines); this.addLines(lines);
currentLineNumber += (lines.length - 1);
} }
}) })
.always((_, textStatus) => { .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.sourceTimeout) clearTimeout(this.sourceTimeout);
if (this.sourceRequest) this.sourceRequest.abort(); if (this.sourceRequest) this.sourceRequest.abort();
} }
componentWillUnmount(){
this.tearDownDynamicSource();
}
setRef(domNode){ setRef(domNode){
if (domNode != null){ if (domNode != null){
this.$console = $(domNode); this.$console = $(domNode);
@ -91,7 +100,6 @@ class Console extends React.Component {
render() { render() {
const prettyLine = (line) => { const prettyLine = (line) => {
// TODO Escape
return {__html: prettyPrintOne(Utils.escapeHtml(line), this.props.lang, this.props.lines)}; return {__html: prettyPrintOne(Utils.escapeHtml(line), this.props.lang, this.props.lines)};
} }
let i = 0; let i = 0;

Wyświetl plik

@ -5,7 +5,7 @@ import ProjectList from './components/ProjectList';
class Dashboard extends React.Component { class Dashboard extends React.Component {
render() { render() {
return ( return (
<ProjectList source="/api/projects/"/> <ProjectList source="/api/projects/?ordering=-created_at"/>
); );
} }
} }

Wyświetl plik

@ -1,18 +1,23 @@
const CANCEL = 1, const CANCEL = 1,
DELETE = 2; REMOVE = 2,
RESTART = 3;
let pendingActions = { let pendingActions = {
[CANCEL]: { [CANCEL]: {
descr: "Canceling..." descr: "Canceling..."
}, },
[DELETE]: { [REMOVE]: {
descr: "Deleting..." descr: "Deleting..."
},
[RESTART]: {
descr: "Restarting..."
} }
}; };
export default { export default {
CANCEL: CANCEL, CANCEL: CANCEL,
DELETE: DELETE, REMOVE: REMOVE,
RESTART: RESTART,
description: function(pendingAction) { description: function(pendingAction) {
if (pendingActions[pendingAction]) return pendingActions[pendingAction].descr; if (pendingActions[pendingAction]) return pendingActions[pendingAction].descr;

Wyświetl plik

@ -15,7 +15,7 @@ let statusCodes = {
}, },
[FAILED]: { [FAILED]: {
descr: "Failed", descr: "Failed",
icon: "fa fa-remove" icon: "fa fa-frown-o"
}, },
[COMPLETED]: { [COMPLETED]: {
descr: "Completed", descr: "Completed",

Wyświetl plik

@ -74,9 +74,9 @@ class ProjectListItem extends React.Component {
}); });
this.dz.on("totaluploadprogress", (progress, totalBytes, totalBytesSent) => { this.dz.on("totaluploadprogress", (progress, totalBytes, totalBytesSent) => {
this.setUploadState({ this.setUploadState({
progress, totalBytes, totalBytesSent progress, totalBytes, totalBytesSent
}); });
}) })
.on("addedfile", () => { .on("addedfile", () => {
this.setUploadState({ this.setUploadState({
@ -167,7 +167,6 @@ class ProjectListItem extends React.Component {
this.setState({ this.setState({
showTaskList: !this.state.showTaskList showTaskList: !this.state.showTaskList
}); });
console.log(this.props);
} }
closeUploadError(){ 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> <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> </div>
</li> </li>

Wyświetl plik

@ -13,6 +13,7 @@ class TaskList extends React.Component {
}; };
this.refresh = this.refresh.bind(this); this.refresh = this.refresh.bind(this);
this.deleteTask = this.deleteTask.bind(this);
} }
componentDidMount(){ componentDidMount(){
@ -46,6 +47,12 @@ class TaskList extends React.Component {
this.taskListRequest.abort(); this.taskListRequest.abort();
} }
deleteTask(id){
this.setState({
tasks: this.state.tasks.filter(t => t.id !== id)
});
}
render() { render() {
let message = ""; let message = "";
if (this.state.loading){ if (this.state.loading){
@ -61,7 +68,7 @@ class TaskList extends React.Component {
{message} {message}
{this.state.tasks.map(task => ( {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> </div>
); );

Wyświetl plik

@ -27,9 +27,9 @@ class TaskListItem extends React.Component {
shouldRefresh(){ shouldRefresh(){
// If a task is completed, or failed, etc. we don't expect it to change // If a task is completed, or failed, etc. we don't expect it to change
return (this.state.task.status === statusCodes.QUEUED || return (([statusCodes.QUEUED, statusCodes.RUNNING, null].indexOf(this.state.task.status) !== -1 && this.state.task.processing_node) ||
this.state.task.status === statusCodes.RUNNING || (!this.state.task.uuid && this.state.task.processing_node && !this.state.task.last_error) ||
(!this.state.task.uuid && this.state.task.processing_node && !this.state.task.last_error)); this.state.task.pending_action !== null);
} }
loadTimer(startTime){ loadTimer(startTime){
@ -65,6 +65,7 @@ class TaskListItem extends React.Component {
// Update timer if we switched to running // Update timer if we switched to running
if (oldStatus !== this.state.task.status){ if (oldStatus !== this.state.task.status){
if (this.state.task.status === statusCodes.RUNNING){ if (this.state.task.status === statusCodes.RUNNING){
this.console.clear();
this.loadTimer(this.state.task.processing_time); this.loadTimer(this.state.task.processing_time);
}else{ }else{
this.setState({time: this.state.task.processing_time}); this.setState({time: this.state.task.processing_time});
@ -74,18 +75,21 @@ class TaskListItem extends React.Component {
}else{ }else{
console.warn("Cannot refresh task: " + json); console.warn("Cannot refresh task: " + json);
} }
if (this.shouldRefresh()) this.refreshTimeout = setTimeout(() => this.refresh(), this.props.refreshInterval || 3000);
}) })
.always((_, textStatus) => { .fail(( _, __, errorThrown) => {
if (textStatus !== "abort"){ if (errorThrown === "Not Found"){ // Don't translate this one
if (this.shouldRefresh()) this.refreshTimeout = setTimeout(() => this.refresh(), this.props.refreshInterval || 3000); // Assume this has been deleted
if (this.props.onDelete) this.props.onDelete(this.state.task.id);
} }
}); });
} }
componentWillUnmount(){ componentWillUnmount(){
this.unloadTimer(); this.unloadTimer();
if (this.refreshTimeout) clearTimeout(this.refreshTimeout);
if (this.refreshRequest) this.refreshRequest.abort(); if (this.refreshRequest) this.refreshRequest.abort();
if (this.refreshTimeout) clearTimeout(this.refreshTimeout);
} }
toggleExpanded(){ toggleExpanded(){
@ -118,38 +122,58 @@ class TaskListItem extends React.Component {
return [pad(h), pad(m), pad(s)].join(':'); return [pad(h), pad(m), pad(s)].join(':');
} }
genActionApiCall(action){ genActionApiCall(action, options = {}){
return () => { return () => {
this.setState({actionButtonsDisabled: true}); const doAction = () => {
this.setState({actionButtonsDisabled: true});
$.post(`/api/projects/${this.state.task.project}/tasks/${this.state.task.id}/${action}/`, $.post(`/api/projects/${this.state.task.project}/tasks/${this.state.task.id}/${action}/`,
{ {
uuid: this.state.task.uuid uuid: this.state.task.uuid
} }
).done(json => { ).done(json => {
if (json.success){ if (json.success){
this.refresh(); this.refresh();
}else{ if (options.success !== undefined) options.success();
}else{
this.setState({
actionError: json.error,
actionButtonsDisabled: false
});
}
})
.fail(() => {
this.setState({ this.setState({
actionError: json.error, error: url + " is unreachable.",
actionButtonsDisabled: false actionButtonsDisabled: false
}); });
} });
}) }
.fail(() => {
this.setState({ if (options.confirm){
error: url + " is unreachable.", if (window.confirm(options.confirm)){
actionButtonsDisabled: false 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() { render() {
let name = this.state.task.name !== null ? this.state.task.name : `Task #${this.state.task.id}`; let name = this.state.task.name !== null ? this.state.task.name : `Task #${this.state.task.id}`;
let status = statusCodes.description(this.state.task.status); let status = statusCodes.description(this.state.task.status);
if (status === "") status = "Uploading images"; 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); if (this.state.task.pending_action !== null) status = pendingActions.description(this.state.task.pending_action);
let expanded = ""; 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("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", () => { // addActionButton("Edit", "btn-primary", "glyphicon glyphicon-pencil", () => {
// console.log("edit call"); // console.log("edit call");
// }); // });
// addActionButton("Delete", "btn-danger", "glyphicon glyphicon-trash", () => { addActionButton("Delete", "btn-danger", "glyphicon glyphicon-trash", this.genActionApiCall("remove", {
// console.log("Delete call"); confirm: "All information related to this task, including images, maps and models will be deleted. Continue?"
// }); }));
actionButtons = (<div className="action-buttons"> actionButtons = (<div className="action-buttons">
{actionButtons.map(button => { {actionButtons.map(button => {
@ -195,6 +230,9 @@ class TaskListItem extends React.Component {
</div> </div>
<div className="labels"> <div className="labels">
<strong>Status: </strong> {status}<br/> <strong>Status: </strong> {status}<br/>
</div>
<div className="labels">
<strong>Options: </strong> {this.optionsToList(this.state.task.options)}<br/>
</div> </div>
{/* TODO: List of images? */} {/* TODO: List of images? */}
</div> </div>
@ -203,7 +241,9 @@ class TaskListItem extends React.Component {
source={this.consoleOutputUrl} source={this.consoleOutputUrl}
refreshInterval={this.shouldRefresh() ? 3000 : undefined} refreshInterval={this.shouldRefresh() ? 3000 : undefined}
autoscroll={true} autoscroll={true}
height={200} /> height={200}
ref={domNode => this.console = domNode}
/>
</div> </div>
</div> </div>
<div className="row"> <div className="row">

Wyświetl plik

@ -16,7 +16,6 @@ class UploadProgressBar extends React.Component {
let percentage = (this.props.progress !== undefined ? let percentage = (this.props.progress !== undefined ?
this.props.progress : this.props.progress :
0).toFixed(2); 0).toFixed(2);
let bytes = this.props.totalBytesSent !== undefined && this.props.totalBytes !== undefined ? let bytes = this.props.totalBytesSent !== undefined && this.props.totalBytes !== undefined ?
`, remaining to upload: ${this.bytesToSize(this.props.totalBytes - this.props.totalBytesSent)}` : `, remaining to upload: ${this.bytesToSize(this.props.totalBytes - this.props.totalBytesSent)}` :
""; "";

Wyświetl plik

@ -586,11 +586,13 @@
eventName = _ref1[_i]; eventName = _ref1[_i];
this.on(eventName, this.options[eventName]); this.on(eventName, this.options[eventName]);
} }
this.on("uploadprogress", (function(_this) { this.on("uploadprogress", (function(_this) {
return function() { return function() {
return _this.updateTotalUploadProgress(); return _this.updateTotalUploadProgress();
}; };
})(this)); })(this));
this.on("removedfile", (function(_this) { this.on("removedfile", (function(_this) {
return function() { return function() {
return _this.updateTotalUploadProgress(); return _this.updateTotalUploadProgress();
@ -703,8 +705,9 @@
_ref = this.getActiveFiles(); _ref = this.getActiveFiles();
for (_i = 0, _len = _ref.length; _i < _len; _i++) { for (_i = 0, _len = _ref.length; _i < _len; _i++) {
file = _ref[_i]; file = _ref[_i];
totalBytesSent += file.upload.bytesSent; totalBytesSent = file.upload.bytesSent;
totalBytes += file.upload.total; totalBytes = file.upload.total;
break;
} }
totalUploadProgress = 100 * totalBytesSent / totalBytes; totalUploadProgress = 100 * totalBytesSent / totalBytes;
} else { } else {
@ -1288,6 +1291,7 @@
for (_l = 0, _len3 = files.length; _l < _len3; _l++) { for (_l = 0, _len3 = files.length; _l < _len3; _l++) {
file = files[_l]; file = files[_l];
_results.push(_this.emit("uploadprogress", file, progress, file.upload.bytesSent)); _results.push(_this.emit("uploadprogress", file, progress, file.upload.bytesSent));
break;
} }
return _results; return _results;
}; };

Wyświetl plik

@ -37,6 +37,11 @@ class TestApi(BootTestCase):
self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertEqual(res.status_code, status.HTTP_200_OK)
self.assertTrue(len(res.data["results"]) > 0) 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)) res = client.get('/api/projects/{}/'.format(project.id))
self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertEqual(res.status_code, status.HTTP_200_OK)
@ -77,11 +82,11 @@ class TestApi(BootTestCase):
self.assertTrue(len(res.data) == 2) self.assertTrue(len(res.data) == 2)
# Can sort # 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[0]['id'] == task.id)
self.assertTrue(res.data[1]['id'] == task2.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[0]['id'] == task2.id)
self.assertTrue(res.data[1]['id'] == task.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)) res = client.post('/api/projects/{}/tasks/{}/cancel/'.format(other_project.id, other_task.id))
self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND) self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)
# TODO: test restart and delete operations
def test_processingnodes(self): def test_processingnodes(self):
client = APIClient() client = APIClient()

Wyświetl plik

@ -34,6 +34,11 @@ class ApiClient:
def task_cancel(self, uuid): def task_cancel(self, uuid):
return requests.post(self.url('/task/cancel'), data={'uuid': uuid}).json() 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=[]): def new_task(self, images, name=None, options=[]):
""" """

@ -1 +1 @@
Subproject commit fdc55291525c03dbdf922724f1a4af8d1a89d2af Subproject commit 038940860d043439b184c4fbd990f8a71b60c324

Wyświetl plik

@ -80,10 +80,12 @@ class ProcessingNode(models.Model):
""" """
api_client = self.api_client() api_client = self.api_client()
result = api_client.task_info(uuid) result = api_client.task_info(uuid)
if result['uuid']: if isinstance(result, dict) and 'uuid' in result:
return result return result
elif result['error']: elif isinstance(result, dict) and 'error' in result:
raise ProcessingException(result['error']) raise ProcessingException(result['error'])
else:
raise ProcessingException("Unknown result from task info: {}".format(result))
def get_task_console_output(self, uuid, line): def get_task_console_output(self, uuid, line):
""" """
@ -105,7 +107,21 @@ class ProcessingNode(models.Model):
""" """
api_client = self.api_client() api_client = self.api_client()
return self.handle_generic_post_response(api_client.task_cancel(uuid)) 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 @staticmethod
def handle_generic_post_response(result): def handle_generic_post_response(result):
""" """

Wyświetl plik

@ -97,7 +97,20 @@ class TestClientApi(TestCase):
self.assertRaises(ProcessingException, online_node.get_task_console_output, "wrong-uuid", 0) 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 # Can cancel task
self.assertTrue(online_node.cancel_task(uuid)) self.assertTrue(online_node.cancel_task(uuid))
self.assertRaises(ProcessingException, online_node.cancel_task, "wrong-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)

Wyświetl plik

@ -212,6 +212,7 @@ REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': [ 'DEFAULT_FILTER_BACKENDS': [
'rest_framework.filters.DjangoObjectPermissionsFilter', 'rest_framework.filters.DjangoObjectPermissionsFilter',
'rest_framework.filters.DjangoFilterBackend', 'rest_framework.filters.DjangoFilterBackend',
'rest_framework.filters.OrderingFilter',
], ],
'PAGE_SIZE': 10, 'PAGE_SIZE': 10,
} }