Projet

Général

Profil

0001-jobs-restart-failed-jobs-42846.patch

Lauréline Guérin, 01 juin 2020 16:35

Télécharger (14,7 ko)

Voir les différences:

Subject: [PATCH] jobs: restart failed jobs (#42846)

 passerelle/base/models.py                     | 17 ++++++
 passerelle/base/views.py                      | 28 ++++++++--
 passerelle/static/js/passerelle.js            | 52 +++++++++---------
 .../includes/resource-jobs-table.html         |  4 +-
 .../templates/passerelle/manage/job.html      | 13 +++++
 .../passerelle/manage/service_jobs.html       |  8 +++
 passerelle/urls.py                            | 17 +++---
 tests/test_manager.py                         | 54 ++++++++++++++++++-
 8 files changed, 155 insertions(+), 38 deletions(-)
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
-