[Feature] Async Prediction Status Poll (#93)

Fixed #81

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#93
This commit is contained in:
2025-09-09 20:39:26 +12:00
parent 5477b5b3d4
commit 3453a169e1
3 changed files with 68 additions and 8 deletions

View File

@ -1358,14 +1358,17 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin):
return self.kv.get('mode', 'build') == 'predicted' return self.kv.get('mode', 'build') == 'predicted'
# Status # Status
def status(self):
return self.kv.get('status', 'completed')
def completed(self): def completed(self):
return self.kv.get('status', 'completed') == 'completed' return self.status() == 'completed'
def running(self): def running(self):
return self.kv.get('status', 'completed') == 'running' return self.status() == 'running'
def failed(self): def failed(self):
return self.kv.get('status', 'completed') == 'failed' return self.status() == 'failed'
def d3_json(self): def d3_json(self):
# Ideally it would be something like this but # Ideally it would be something like this but
@ -1472,7 +1475,8 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin):
"upToDate": True, "upToDate": True,
"links": adjusted_links, "links": adjusted_links,
"nodes": nodes, "nodes": nodes,
"modified": self.modified.strftime('%Y-%m-%d %H:%M:%S') "modified": self.modified.strftime('%Y-%m-%d %H:%M:%S'),
"status": self.status(),
} }
return json.dumps(res) return json.dumps(res)

View File

@ -1438,6 +1438,12 @@ def package_pathway(request, package_uuid, pathway_uuid):
if request.GET.get("last_modified", False): if request.GET.get("last_modified", False):
return JsonResponse({'modified': current_pathway.modified.strftime('%Y-%m-%d %H:%M:%S')}) return JsonResponse({'modified': current_pathway.modified.strftime('%Y-%m-%d %H:%M:%S')})
if request.GET.get('status', False):
return JsonResponse({
'status': current_pathway.status(),
'modified': current_pathway.modified.strftime('%Y-%m-%d %H:%M:%S'),
})
if request.GET.get("download", False) == "true": if request.GET.get("download", False) == "true":
filename = f"{current_pathway.name.replace(' ', '_')}_{current_pathway.uuid}.csv" filename = f"{current_pathway.name.replace(' ', '_')}_{current_pathway.uuid}.csv"
csv_pw = current_pathway.to_csv() csv_pw = current_pathway.to_csv()

View File

@ -149,19 +149,19 @@
</li> </li>
<li> <li>
{% if pathway.completed %} {% if pathway.completed %}
<button type="button" class="btn btn-default navbar-btn" data-toggle="tooltip" <button type="button" class="btn btn-default navbar-btn" data-toggle="popover"
id="status" data-original-title="" title="" id="status" data-original-title="" title=""
data-content="Pathway prediction complete."> data-content="Pathway prediction complete.">
<span class="glyphicon glyphicon-ok"></span> <span class="glyphicon glyphicon-ok"></span>
</button> </button>
{% elif pathway.failed %} {% elif pathway.failed %}
<button type="button" class="btn btn-default navbar-btn" data-toggle="tooltip" <button type="button" class="btn btn-default navbar-btn" data-toggle="popover"
id="status" data-original-title="" title="" id="status" data-original-title="" title=""
data-content="Pathway prediction failed."> data-content="Pathway prediction failed.">
<span class="glyphicon glyphicon-remove"></span> <span class="glyphicon glyphicon-remove"></span>
</button> </button>
{% else %} {% else %}
<button type="button" class="btn btn-default navbar-btn" data-toggle="tooltip" <button type="button" class="btn btn-default navbar-btn" data-toggle="popover"
id="status" data-original-title="" title="" id="status" data-original-title="" title=""
data-content="Pathway prediction running."> data-content="Pathway prediction running.">
<img height="20" src="{% static '/images/wait.gif' %}"> <img height="20" src="{% static '/images/wait.gif' %}">
@ -317,7 +317,7 @@
</div> </div>
</div> </div>
<script> <script>
// Globla switch for app domain view // Global switch for app domain view
var appDomainViewEnabled = false; var appDomainViewEnabled = false;
function goFullscreen(id) { function goFullscreen(id) {
@ -336,6 +336,56 @@
pathway = {{ pathway.d3_json | safe }}; pathway = {{ pathway.d3_json | safe }};
$(function () { $(function () {
$('#status').popover({
trigger: 'manual',
placement: 'bottom',
html: true
});
// If prediction is still running, regularly check status
if (pathway.status === 'running') {
let last_modified = pathway.modified;
let pollInterval = setInterval(async () => {
try {
const response = await fetch("{{ pathway.url }}?status=true", {});
const data = await response.json();
if (data.modified > last_modified) {
var msg = 'Prediction '
var btn = '<button type="button" onclick="location.reload()" class="btn btn-primary" id="reloadBtn">Reload page</button>'
if (data.status === "running") {
msg += 'is still running. But the Pathway was updated.<br>' + btn;
} else if (data.status === "completed") {
msg += 'is completed. Reload the page to see the updated Pathway.<br>' + btn;
} else if (data.status === "failed") {
msg += 'failed. Reload the page to see the current shape<br>' + btn;
}
$('#status').attr(
'data-content', msg
).popover('show');
}
if (data.status === "completed" || data.status === "failed") {
$('#status img').remove();
if (data.status === "completed") {
$('#status').append('<span class="glyphicon glyphicon-ok"></span>')
} else {
$('#status').append('<span class="glyphicon glyphicon-remove"></span>')
}
clearInterval(pollInterval);
}
} catch (err) {
console.error("Polling error:", err);
}
}, 5000);
}
draw(pathway, 'vizdiv'); draw(pathway, 'vizdiv');
// TODO fix somewhere else... // TODO fix somewhere else...
var newDesc = transformReferences($('#DescriptionContent')[0].innerText); var newDesc = transformReferences($('#DescriptionContent')[0].innerText);