Skip to content

Commit 149869f

Browse files
authored
[Fixes #13471] Synchronization of the services UI with the harvesting processes (#13473)
[Fixes #13471] Synchronization of the services UI with the harvesting processes (#13473)
1 parent 1deedb4 commit 149869f

File tree

5 files changed

+186
-97
lines changed

5 files changed

+186
-97
lines changed

geonode/services/templates/services/service_detail.html

Lines changed: 72 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -14,43 +14,22 @@ <h3><strong>{{service.title|default:service.name}}</strong></h3>
1414
<p><strong>SERVICE NOTES:</strong> The service is accessed by Basic auth via the user <strong>{{service.username}}</strong></p>
1515
{% endif %}
1616
{% autoescape off %}
17-
<h3>{% trans "Service Resources" %} <span class="badge">{{ total_resources }}</span></h3>
18-
{% if service.harvester %}
19-
{% with service.harvester.latest_harvesting_session.get_progress_percentage as harvesting_progress %}
20-
{% if harvesting_progress >= 0 and harvesting_progress < 100.0 %}
21-
<p>
22-
<i class="fa fa-gear"></i> <i>{% trans "Resource harvesting in progress..." %}</i>
23-
<div class="progress">
24-
<div class="progress-bar" role="progressbar" style="width: {{ harvesting_progress }}%;" aria-valuenow="{{ harvesting_progress }}" aria-valuemin="0" aria-valuemax="100">{{ harvesting_progress }}%</div>
17+
<div id="harvestProgressWrapper" style="display:none;">
18+
<i class="fa fa-gear"></i> <i id="harvestProgressText"></i>
19+
<div class="progress" id="harvestProgressContainer">
20+
<div class="progress-bar" id="harvestProgressBar" role="progressbar"
21+
style="width:0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
22+
0%
23+
</div>
2524
</div>
26-
</p>
25+
</div>
26+
<div id="resourcesContainer">
27+
{% if resources %}
28+
{% include "services/service_resources_partial.html" %}
29+
{% else %}
30+
<p>{% trans "No resources have been imported yet." %}</p>
2731
{% endif %}
28-
{% endwith %}
29-
{% endif %}
30-
{% if total_resources == 0 %}
31-
<p>{% trans "No resources have been imported yet." %}</p>
32-
{% else %}
33-
<div class="row">
34-
<table class="table">
35-
<thead>
36-
<th>{% trans "Title" %}</th>
37-
<th>{% trans "Description" %}</th>
38-
</thead>
39-
{% for dataset in datasets %}
40-
<tr>
41-
<td><a href='{{ dataset.get_absolute_url }}'>{{dataset.title|striptags}}</a></td>
42-
<td>
43-
<div class="row">
44-
<div class="col-md-9">
45-
{{dataset.abstract|striptags}}
46-
</div>
47-
</div>
48-
</td>
49-
</tr>
50-
{% endfor %}
51-
</table>
52-
</div>
53-
{% endif %}
32+
</div>
5433
{% if resources.paginator.num_pages > 1 %}
5534
<div class="row">
5635
<nav aria-label="importable resources pages">
@@ -109,27 +88,64 @@ <h3>{% trans "Service Resources" %} <span class="badge">{{ total_resources }}</s
10988
{% endif %}
11089
{% endblock %}
11190
{% block extra_script %}
112-
{{ block.super }}
113-
<script type="text/javascript">
114-
$(document).ready(function () {
115-
$('#harvestResources').on("click", function() {
116-
$("#progressModal").modal("show");
117-
});
118-
$('button[name^=retry]').on('click', function () {
119-
const resourceId = this.name.replace("retry-", "");
120-
const retryUrl = "/services/{{ service.id }}/harvest/" + resourceId;
121-
const retryForm = document.createElement('form');
122-
retryForm.setAttribute("method", "post");
123-
retryForm.setAttribute("action", retryUrl);
124-
const csrfInputElem = document.createElement("input");
125-
csrfInputElem.type = "hidden";
126-
csrfInputElem.name = "csrfmiddlewaretoken";
127-
csrfInputElem.value = "{{ csrf_token }}";
128-
retryForm.appendChild(csrfInputElem);
129-
document.body.appendChild(retryForm);
130-
retryForm.submit();
91+
{{ block.super }}
92+
<script type="text/javascript">
93+
$(document).ready(function () {
94+
$('#harvestResources').on("click", function() {
95+
$("#progressModal").modal("show");
96+
});
13197

132-
});
98+
$('button[name^=retry]').on('click', function () {
99+
const resourceId = this.name.replace("retry-", "");
100+
const retryUrl = "/services/{{ service.id }}/harvest/" + resourceId;
101+
const retryForm = document.createElement('form');
102+
retryForm.setAttribute("method", "post");
103+
retryForm.setAttribute("action", retryUrl);
104+
const csrfInputElem = document.createElement("input");
105+
csrfInputElem.type = "hidden";
106+
csrfInputElem.name = "csrfmiddlewaretoken";
107+
csrfInputElem.value = "{{ csrf_token }}";
108+
retryForm.appendChild(csrfInputElem);
109+
document.body.appendChild(retryForm);
110+
retryForm.submit();
133111
});
134-
</script>
112+
113+
114+
let lastStatus = null;
115+
let resourcesUpdated = false;
116+
117+
function updateHarvestProgress() {
118+
$.get("{% url 'service_harvest_progress' service.id %}", function(data) {
119+
let progress = data.progress || 0;
120+
121+
if (data.in_progress) {
122+
$("#harvestProgressWrapper").show();
123+
$("#harvestProgressText").text("Resource harvesting in progress...");
124+
$("#harvestProgressBar")
125+
.css("width", progress + "%")
126+
.attr("aria-valuenow", progress)
127+
.text(Math.round(progress) + "%");
128+
resourcesUpdated = false; // reset flag while harvesting
129+
} else {
130+
$("#harvestProgressWrapper").hide();
131+
$("#harvestProgressText").text("");
132+
$("#harvestProgressBar")
133+
.css("width", "0%")
134+
.attr("aria-valuenow", 0)
135+
.text("0%");
136+
137+
// Trigger partial API exactly once when in-progress → finished
138+
if (lastStatus === "on-going" && !resourcesUpdated) {
139+
$("#resourcesContainer").load("{% url 'service_resources_partial' service.id %}");
140+
resourcesUpdated = true;
141+
}
142+
}
143+
144+
lastStatus = data.status;
145+
});
146+
}
147+
updateHarvestProgress();
148+
setInterval(updateHarvestProgress, 7000)
149+
});
150+
</script>
135151
{% endblock extra_script %}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{% load i18n %}
2+
3+
<h3>
4+
{% trans "Service Resources" %}
5+
<span class="badge">{{ total_resources }}</span>
6+
</h3>
7+
8+
{% if total_resources == 0 %}
9+
<p>{% trans "No resources have been imported yet." %}</p>
10+
{% else %}
11+
<div class="row">
12+
<table class="table">
13+
<thead>
14+
<th>{% trans "Title" %}</th>
15+
<th>{% trans "Description" %}</th>
16+
</thead>
17+
{% for resource in resources %}
18+
<tr>
19+
<td><a href="{{ resource.get_absolute_url }}">{{ resource.title|striptags }}</a></td>
20+
<td>
21+
<div class="row">
22+
<div class="col-md-9">
23+
{{ resource.abstract|striptags }}
24+
</div>
25+
</div>
26+
</td>
27+
</tr>
28+
{% endfor %}
29+
</table>
30+
</div>
31+
{% endif %}

geonode/services/templatetags/services_tags.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def get_dataset_count_by_services(service_id, user):
3333
if service.harvester:
3434
_h = service.harvester
3535
harvested_resources_ids = list(
36-
_h.harvestable_resources.filter(should_be_harvested=True, geonode_resource__isnull=False).values_list(
36+
_h.harvestable_resources.filter(geonode_resource__isnull=False).values_list(
3737
"geonode_resource__id", flat=True
3838
)
3939
)

geonode/services/urls.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,14 @@
3535
views.harvest_single_resource,
3636
name="harvest_single_resource",
3737
),
38+
re_path(
39+
r"^(?P<service_id>\d+)/progress/$",
40+
views.service_harvest_progress,
41+
name="service_harvest_progress",
42+
),
43+
re_path(
44+
r"^(?P<service_id>\d+)/resources_partial/$",
45+
views.service_resources_partial,
46+
name="service_resources_partial",
47+
),
3848
]

geonode/services/views.py

Lines changed: 72 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from django.contrib import messages
2323
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
2424
from django.urls import reverse
25-
from django.http import HttpResponse, HttpResponseRedirect
25+
from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
2626
from django.http import Http404
2727
from django.shortcuts import get_object_or_404
2828
from django.shortcuts import redirect
@@ -241,46 +241,12 @@ def rescan_service(request, service_id):
241241
def service_detail(request, service_id):
242242
"""This view shows the details of a service"""
243243

244-
services = Service.objects.filter(resourcebase_ptr_id=service_id)
245-
246-
if not services.exists():
247-
messages.add_message(request, messages.ERROR, _("You dont have enougth rigths to see the resource detail"))
248-
return redirect(reverse("services"))
249-
service = services.first()
244+
service, resources, total_resources = _get_service_and_resources(service_id, request.user, request.GET.get("page"))
250245

251246
permissions_json = _perms_info_json(service)
252247

253248
perms_list = permissions_registry.get_perms(instance=service, user=request.user)
254249

255-
harvested_resources_ids = []
256-
if service.harvester:
257-
_h = service.harvester
258-
harvested_resources_ids = list(
259-
_h.harvestable_resources.filter(should_be_harvested=True, geonode_resource__isnull=False).values_list(
260-
"geonode_resource__id", flat=True
261-
)
262-
)
263-
already_imported_datasets = get_visible_resources(
264-
queryset=ResourceBase.objects.filter(id__in=harvested_resources_ids), user=request.user
265-
)
266-
resources_being_harvested = []
267-
268-
all_resources = list(resources_being_harvested) + list(already_imported_datasets)
269-
270-
paginator = Paginator(all_resources, getattr(settings, "CLIENT_RESULTS_LIMIT", 25), orphans=3)
271-
page = request.GET.get("page")
272-
try:
273-
resources = paginator.page(page)
274-
except PageNotAnInteger:
275-
resources = paginator.page(1)
276-
except EmptyPage:
277-
resources = paginator.page(paginator.num_pages)
278-
279-
# pop the handler out of the session in order to free resources
280-
# - we had stored the service handler on the session in order to
281-
# speed up the register/harvest resources flow. However, for services
282-
# with many resources, keeping the handler in the session leads to degraded
283-
# performance
284250
try:
285251
request.session.pop(service.service_url)
286252
except KeyError:
@@ -291,14 +257,12 @@ def service_detail(request, service_id):
291257
template_name="services/service_detail.html",
292258
context={
293259
"service": service,
294-
"datasets": already_imported_datasets,
295-
# "resource_jobs": (r for r in resources if isinstance(r, HarvestJob)),
260+
"resources": resources,
296261
"resource_jobs": (),
297262
"permissions_json": permissions_json,
298263
"permissions_list": perms_list,
299264
"can_add_resorces": request.user.has_perm("base.add_resourcebase"),
300-
"resources": resources,
301-
"total_resources": len(already_imported_datasets),
265+
"total_resources": total_resources,
302266
},
303267
)
304268

@@ -352,3 +316,71 @@ def remove_service(request, service_id):
352316
service.harvester.delete()
353317
messages.add_message(request, messages.INFO, _(f"Service {service.title} has been deleted"))
354318
return HttpResponseRedirect(reverse("services"))
319+
320+
321+
@login_required
322+
def service_harvest_progress(request, service_id):
323+
try:
324+
service = Service.objects.get(id=service_id)
325+
session = None
326+
if service.harvester:
327+
# Get latest active session if any
328+
active_session = service.harvester.latest_harvesting_session
329+
if active_session and active_session.status in ["pending", "on-going"]:
330+
session = active_session
331+
if session:
332+
progress = session.get_progress_percentage()
333+
status = session.status
334+
in_progress = status in ["pending", "on-going"]
335+
else:
336+
progress = None
337+
status = "no-session"
338+
in_progress = False
339+
return JsonResponse({"progress": progress, "status": status, "in_progress": in_progress})
340+
except Service.DoesNotExist:
341+
return JsonResponse({"progress": 0, "status": "not_found", "in_progress": False})
342+
343+
344+
@login_required
345+
def service_resources_partial(request, service_id):
346+
service, resources, total_resources = _get_service_and_resources(service_id, request.user, request.GET.get("page"))
347+
348+
return render(
349+
request,
350+
template_name="services/service_resources_partial.html",
351+
context={
352+
"service": service,
353+
"resources": resources,
354+
"total_resources": total_resources,
355+
},
356+
)
357+
358+
359+
def _get_service_and_resources(service_id, user, page):
360+
"""
361+
Common helper to fetch a service and its paginated resources.
362+
"""
363+
service = get_object_or_404(Service, id=service_id)
364+
365+
harvested_resources_ids = []
366+
if service.harvester:
367+
harvested_resources_ids = list(
368+
service.harvester.harvestable_resources.filter(geonode_resource__isnull=False).values_list(
369+
"geonode_resource__id", flat=True
370+
)
371+
)
372+
373+
datasets = get_visible_resources(queryset=ResourceBase.objects.filter(id__in=harvested_resources_ids), user=user)
374+
375+
resources_being_harvested = []
376+
all_resources = list(resources_being_harvested) + list(datasets)
377+
378+
paginator = Paginator(all_resources, getattr(settings, "CLIENT_RESULTS_LIMIT", 25), orphans=3)
379+
try:
380+
resources = paginator.page(page)
381+
except PageNotAnInteger:
382+
resources = paginator.page(1)
383+
except EmptyPage:
384+
resources = paginator.page(paginator.num_pages)
385+
386+
return service, resources, len(datasets)

0 commit comments

Comments
 (0)