0001-jobs-restart-failed-jobs-42846.patch
passerelle/base/models.py | ||
---|---|---|
725 | 725 |
choices=(('registered', _('Registered')), |
726 | 726 |
('running', _('Running')), |
727 | 727 |
('failed', _('Failed')), |
728 |
('restarted', _('Failed and restarted')), |
|
728 | 729 |
('completed', _('Completed')) |
729 | 730 |
), |
730 | 731 |
) |
... | ... | |
740 | 741 |
else: |
741 | 742 |
self.after_timestamp = value |
742 | 743 | |
744 |
def restart(self): |
|
745 |
# clone current job |
|
746 |
new_job = copy.deepcopy(self) |
|
747 |
new_job.pk = None |
|
748 |
# set status |
|
749 |
new_job.status = 'registered' |
|
750 |
# reset some fields |
|
751 |
new_job.done_timestamp = None |
|
752 |
new_job.status_details = {} |
|
753 |
new_job.save() |
|
754 | ||
755 |
# change current job status |
|
756 |
self.status = 'restarted' |
|
757 |
self.status_details.update({'new_job_pk': new_job.pk}) |
|
758 |
self.save() |
|
759 | ||
743 | 760 | |
744 | 761 |
@six.python_2_unicode_compatible |
745 | 762 |
class ResourceLog(models.Model): |
passerelle/base/views.py | ||
---|---|---|
20 | 20 |
from dateutil import parser as date_parser |
21 | 21 | |
22 | 22 |
from django.contrib.contenttypes.models import ContentType |
23 |
from django.contrib import messages |
|
24 | 23 |
from django.core.urlresolvers import reverse |
25 | 24 |
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied |
26 | 25 |
from django.db.models import Q |
27 | 26 |
from django.forms import models as model_forms |
28 | 27 |
from django.views.generic import ( |
29 | 28 |
View, DetailView, ListView, CreateView, UpdateView, DeleteView, FormView) |
30 |
from django.http import Http404, HttpResponse |
|
29 |
from django.http import Http404, HttpResponse, HttpResponseRedirect |
|
30 |
from django.shortcuts import get_object_or_404 |
|
31 | 31 |
from django.utils.timezone import make_aware |
32 | 32 |
from django.utils.translation import ugettext_lazy as _ |
33 | 33 | |
... | ... | |
225 | 225 | |
226 | 226 |
def get_context_data(self, **kwargs): |
227 | 227 |
context = super(GenericViewJobsConnectorView, self).get_context_data(**kwargs) |
228 |
context['object'] = self.get_object() |
|
228 |
connector = self.get_object() |
|
229 |
context['object'] = connector |
|
229 | 230 |
context['query'] = self.request.GET.get('q') or '' |
231 |
if self.request.GET.get('job_id'): |
|
232 |
try: |
|
233 |
resource_type = ContentType.objects.get_for_model(connector) |
|
234 |
context['job_target'] = Job.objects.get( |
|
235 |
resource_type=resource_type, |
|
236 |
resource_pk=connector.pk, |
|
237 |
pk=self.request.GET['job_id'] |
|
238 |
) |
|
239 |
except (ValueError, Job.DoesNotExist): |
|
240 |
pass |
|
230 | 241 |
return context |
231 | 242 | |
232 | 243 |
def get_object(self): |
... | ... | |
234 | 245 | |
235 | 246 |
def get_queryset(self): |
236 | 247 |
connector = self.get_object() |
237 |
resource_type = ContentType.objects.get_for_model(connector) |
|
238 | 248 |
qs = connector.jobs_set().order_by('-creation_timestamp') |
239 | 249 |
query = self.request.GET.get('q') |
240 | 250 |
if query: |
... | ... | |
282 | 292 |
return context |
283 | 293 | |
284 | 294 | |
295 |
class GenericRestartJobView(GenericConnectorMixin, View): |
|
296 |
def get(self, request, *args, **kwargs): |
|
297 |
connector = get_object_or_404(self.model, slug=kwargs['slug']) |
|
298 |
resource_type = ContentType.objects.get_for_model(connector) |
|
299 |
job = get_object_or_404(Job, pk=self.kwargs['job_pk'], resource_type=resource_type, resource_pk=connector.pk, status='failed') |
|
300 |
job.restart() |
|
301 |
return HttpResponseRedirect( |
|
302 |
reverse('view-jobs-connector', kwargs={'connector': kwargs['connector'], 'slug': kwargs['slug']})) |
|
303 | ||
304 | ||
285 | 305 |
class ImportSiteView(FormView): |
286 | 306 |
template_name = 'passerelle/manage/import_site.html' |
287 | 307 |
form_class = ImportSiteForm |
passerelle/static/js/passerelle.js | ||
---|---|---|
1 |
open_window = function(base_url, object_pk, object_name) { |
|
2 |
var url = base_url + object_pk + '/'; |
|
3 |
var current_url = window.location; |
|
4 |
var object_key = object_name + '_id'; |
|
5 |
if (window.location.href == window.location.origin + base_url + '?'+ object_key + '=' + object_pk) { |
|
6 |
// remove <object_name>_id from url on modal close if direct access to an object |
|
7 |
window.history.pushState({}, 'no ' + object_name, base_url); |
|
8 |
} |
|
9 |
$.get(url, function(response) { |
|
10 |
var $dialog = $(response).dialog({ |
|
11 |
modal: true, |
|
12 |
width: 'auto', |
|
13 |
open: function(event, ui) { |
|
14 |
window.history.pushState({object_key: object_pk}, object_name + ' id', base_url + '?' + object_key + '=' + object_pk); |
|
15 |
}, |
|
16 |
close: function (event, ui) { |
|
17 |
window.history.back(); |
|
18 |
} |
|
19 |
}); |
|
20 |
}); |
|
21 |
}; |
|
22 | ||
1 | 23 |
open_log_window = function(base_url, log_pk) { |
2 |
var url = base_url + log_pk + '/'; |
|
3 |
var current_url = window.location; |
|
4 |
if (window.location.href == window.location.origin + base_url + '?log_id=' + log_pk) { |
|
5 |
// remove log_id from url on modal close if direct access to a log |
|
6 |
window.history.pushState({}, 'no log', base_url); |
|
7 |
} |
|
8 |
$.get(url, |
|
9 |
function(response) { |
|
10 |
var $dialog = $(response).dialog({ |
|
11 |
modal: true, |
|
12 |
width: 'auto', |
|
13 |
open: function(event, ui) { |
|
14 |
window.history.pushState({'log_id': log_pk}, 'log id', base_url + '?log_id=' + log_pk); |
|
15 |
}, |
|
16 |
close: function (event, ui) { |
|
17 |
window.history.back(); |
|
18 |
} |
|
19 |
}); |
|
20 |
}); |
|
24 |
open_window(base_url, log_pk, 'log'); |
|
25 |
}; |
|
26 | ||
27 |
open_job_window = function(base_url, job_pk) { |
|
28 |
open_window(base_url, job_pk, 'job'); |
|
21 | 29 |
}; |
22 | 30 | |
23 | 31 |
$(function() { |
... | ... | |
29 | 37 |
$('#jobs tbody tr').on('click', function() { |
30 | 38 |
var base_url = $(this).parents('table.main').data('job-base-url'); |
31 | 39 |
var job_pk = $(this).data('pk'); |
32 |
var url = base_url + job_pk + '/'; |
|
33 |
$.get(url, |
|
34 |
function(response) { |
|
35 |
var $dialog = $(response).dialog({modal: true, width: 'auto'}); |
|
36 |
}); |
|
40 |
open_job_window(base_url, job_pk); |
|
37 | 41 |
}); |
38 | 42 | |
39 | 43 |
/* keep title/slug in sync, |
passerelle/templates/passerelle/includes/resource-jobs-table.html | ||
---|---|---|
23 | 23 |
</tbody> |
24 | 24 |
</table> |
25 | 25 | |
26 |
{% with page_obj=logrecords %}
|
|
27 |
{% include "gadjo/pagination.html" with anchor="#jobs" %} |
|
26 |
{% with page_obj=jobs %}
|
|
27 |
{% include "gadjo/pagination.html" with anchor="#jobs" without_key="job_id" %}
|
|
28 | 28 |
{% endwith %} |
29 | 29 | |
30 | 30 |
{% endif %} |
passerelle/templates/passerelle/manage/job.html | ||
---|---|---|
6 | 6 |
<td>{{ job.parameters }}</td> |
7 | 7 |
</tr> |
8 | 8 |
{% for key, value in job.status_details.items %} |
9 |
{% if key != 'new_job_pk' %} |
|
9 | 10 |
<tr> |
10 | 11 |
<td>{{key}}</td> |
11 | 12 |
<td>{% for key2, value2 in value.items %} |
... | ... | |
14 | 15 |
{{value|linebreaksbr}} |
15 | 16 |
{% endfor %}</td> |
16 | 17 |
</tr> |
18 |
{% endif %} |
|
17 | 19 |
{% endfor %} |
18 | 20 |
</table> |
21 | ||
22 |
{% if job.status == 'failed' or job.status == 'restarted' %} |
|
23 |
<div class="buttons"> |
|
24 |
{% if job.status == 'failed' %} |
|
25 |
<a class="button" href="{% url 'restart-job' connector=object.get_connector_slug slug=object.slug job_pk=job.pk %}">{% trans "Restart" %}</a> |
|
26 |
{% else %} |
|
27 |
<a class="button" href="{% url 'view-jobs-connector' connector=object.get_connector_slug slug=object.slug %}?job_id={{ job.status_details.new_job_pk }}">{% trans "See new job" %}</a> |
|
28 |
{% endif %} |
|
29 |
</div> |
|
30 |
{% endif %} |
|
31 | ||
19 | 32 |
</div> |
passerelle/templates/passerelle/manage/service_jobs.html | ||
---|---|---|
24 | 24 |
{% include "passerelle/includes/resource-jobs-table.html" with jobs=page_obj %} |
25 | 25 |
</div> |
26 | 26 | |
27 |
{% if job_target %} |
|
28 |
<script> |
|
29 |
$(function () { |
|
30 |
open_job_window($('table.main').data('job-base-url'), {{ job_target.pk }}); |
|
31 |
}); |
|
32 |
</script> |
|
33 |
{% endif %} |
|
34 | ||
27 | 35 |
{% endblock %} |
passerelle/urls.py | ||
---|---|---|
4 | 4 |
from django.contrib import admin |
5 | 5 | |
6 | 6 |
from django.contrib.auth.decorators import login_required |
7 |
from django.contrib.staticfiles.urls import staticfiles_urlpatterns |
|
7 | 8 |
from django.views.static import serve as static_serve |
8 | 9 | |
9 |
from .views import (HomePageView, ManageView, ManageAddView, |
|
10 |
GenericCreateConnectorView, GenericDeleteConnectorView, |
|
11 |
GenericEditConnectorView, GenericEndpointView, GenericConnectorView, |
|
12 |
GenericViewLogsConnectorView, GenericLogView, GenericExportConnectorView, |
|
13 |
login, logout, menu_json) |
|
14 |
from .base.views import GenericViewJobsConnectorView, GenericJobView |
|
10 |
from .views import ( |
|
11 |
HomePageView, ManageView, ManageAddView, |
|
12 |
GenericCreateConnectorView, GenericDeleteConnectorView, |
|
13 |
GenericEditConnectorView, GenericEndpointView, GenericConnectorView, |
|
14 |
GenericViewLogsConnectorView, GenericLogView, GenericExportConnectorView, |
|
15 |
login, logout, menu_json) |
|
16 |
from .base.views import GenericViewJobsConnectorView, GenericJobView, GenericRestartJobView |
|
15 | 17 |
from .urls_utils import decorated_includes, required, app_enabled, manager_required |
16 | 18 |
from .base.urls import access_urlpatterns, import_export_urlpatterns |
17 | 19 |
from .plugins import register_apps_urls |
... | ... | |
76 | 78 |
GenericViewJobsConnectorView.as_view(), name='view-jobs-connector'), |
77 | 79 |
url(r'^(?P<slug>[\w,-]+)/jobs/(?P<job_pk>\d+)/$', |
78 | 80 |
GenericJobView.as_view(), name='view-job'), |
81 |
url(r'^(?P<slug>[\w,-]+)/jobs/(?P<job_pk>\d+)/restart/$', |
|
82 |
GenericRestartJobView.as_view(), name='restart-job'), |
|
79 | 83 |
url(r'^(?P<slug>[\w,-]+)/export$', |
80 | 84 |
GenericExportConnectorView.as_view(), name='export-connector'), |
81 | 85 |
]))) |
... | ... | |
94 | 98 |
url(r'^__debug__/', include(debug_toolbar.urls)), |
95 | 99 |
] + urlpatterns |
96 | 100 | |
97 |
from django.contrib.staticfiles.urls import staticfiles_urlpatterns |
|
98 | 101 |
urlpatterns += staticfiles_urlpatterns() |
tests/test_manager.py | ||
---|---|---|
7 | 7 |
from django.contrib.contenttypes.models import ContentType |
8 | 8 |
from django.core.files import File |
9 | 9 |
from django.utils.six import StringIO |
10 |
from django.utils.timezone import now |
|
10 | 11 |
import pytest |
11 | 12 | |
12 |
from passerelle.base.models import ApiUser, AccessRight, ResourceLog, ResourceStatus
|
|
13 |
from passerelle.base.models import ApiUser, AccessRight, ResourceStatus, Job
|
|
13 | 14 |
from passerelle.apps.csvdatasource.models import CsvDataSource, Query |
14 | 15 | |
15 | 16 |
pytestmark = pytest.mark.django_db |
... | ... | |
319 | 320 |
resp = resp.form.submit() |
320 | 321 |
assert len(resp.pyquery('#id_notification_delays_p .error')) |
321 | 322 | |
323 | ||
322 | 324 |
def test_jobs(app, admin_user): |
323 | 325 |
data = StringIO('1;Foo\n2;Bar\n3;Baz') |
324 | 326 |
csv = CsvDataSource.objects.create(csv_file=File(data, 't.csv'), |
... | ... | |
372 | 374 |
resp = app.get(base_url + job_pk + '/') |
373 | 375 |
resp = app.get(base_url + '12345' + '/', status=404) |
374 | 376 | |
377 |
resp = app.get('/manage/csvdatasource/test/jobs/?job_id=%s' % job_pk) |
|
378 |
assert 'job_target' in resp.context |
|
379 | ||
380 |
# unknown id |
|
381 |
resp = app.get('/manage/csvdatasource/test/jobs/?job_id=0') |
|
382 |
assert 'job_target' not in resp.context |
|
383 |
# bad id |
|
384 |
resp = app.get('/manage/csvdatasource/test/jobs/?job_id=foo') |
|
385 |
assert 'job_target' not in resp.context |
|
386 | ||
387 | ||
388 |
def test_job_restart(app, admin_user): |
|
389 |
data = StringIO('1;Foo\n2;Bar\n3;Baz') |
|
390 |
csv = CsvDataSource.objects.create( |
|
391 |
csv_file=File(data, 't.csv'), |
|
392 |
columns_keynames='id, text', slug='test', title='a title', description='a description') |
|
393 |
app = login(app) |
|
394 | ||
395 |
# unknown job |
|
396 |
app.get('/manage/csvdatasource/test/jobs/0/restart/', status=404) |
|
397 |
app.get('/manage/csvdatasource/test/jobs/foo/restart/', status=404) |
|
398 | ||
399 |
# create a job, with wrong status |
|
400 |
job = Job.objects.create(resource=csv, status='registered') |
|
401 |
app.get('/manage/csvdatasource/test/jobs/%s/restart/' % job.pk, status=404) |
|
402 | ||
403 |
job.status = 'failed' |
|
404 |
job.done_timestamp = now() |
|
405 |
job.status_details = {'error_summary': 'foo bar'} |
|
406 |
job.save() |
|
407 | ||
408 |
# unknown connector |
|
409 |
app.get('/manage/csvdatasourc/test/jobs/%s/restart/' % job.pk, status=404) |
|
410 |
app.get('/manage/csvdatasource/teste/jobs/%s/restart/' % job.pk, status=404) |
|
411 | ||
412 |
# ok, restart job |
|
413 |
resp = app.get('/manage/csvdatasource/test/jobs/%s/restart/' % job.pk, status=302) |
|
414 |
new_job = Job.objects.latest('pk') |
|
415 |
assert resp.location.endswith('/manage/csvdatasource/test/jobs/') |
|
416 |
assert new_job.status == 'registered' |
|
417 |
assert new_job.done_timestamp is None |
|
418 |
assert new_job.status_details == {} |
|
419 | ||
420 |
job.refresh_from_db() |
|
421 |
assert job.status == 'restarted' |
|
422 |
assert job.status_details == {'error_summary': 'foo bar', 'new_job_pk': new_job.pk} |
|
423 | ||
424 |
resp = app.get('/manage/csvdatasource/test/jobs/%s/' % job.pk) |
|
425 |
assert '/manage/csvdatasource/test/jobs/?job_id=%s' % new_job.pk in resp |
|
426 | ||
375 | 427 | |
376 | 428 |
def test_manager_import_export(app, admin_user): |
377 | 429 |
data = StringIO('1;Foo\n2;Bar\n3;Baz') |
378 |
- |