Skip to content

Commit 4c6c477

Browse files
authored
Merge pull request #33 from okfn/30-pending
Modificando como se muestran los estados
2 parents 6fcfb8a + c518af6 commit 4c6c477

11 files changed

Lines changed: 224 additions & 79 deletions

File tree

ckanext/validate/actions/action.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ def resource_validate(context, data_dict):
6262
"Frictionless validation completed for resource %s: valid=%s",
6363
resource_id, report.valid,
6464
)
65-
log.debug("Validation report for resource %s: %s", resource_id, report.to_descriptor())
6665

6766
status = "success" if report.valid else "failure"
6867

ckanext/validate/assets/css/validate.css

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,14 @@
2929
background-color: #6c757d;
3030
color: #fff;
3131
}
32+
33+
.validate-badge {
34+
display: inline-flex;
35+
align-items: center;
36+
gap: 4px;
37+
}
38+
39+
.validate-badge i {
40+
margin-right: 4px;
41+
}
42+

ckanext/validate/jobs.py

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,42 +13,45 @@
1313
ckan -c /etc/ckan/default/ckan.ini jobs worker
1414
1515
https://docs.ckan.org/en/2.11/maintaining/cli.html
16-
1716
"""
1817

1918

20-
def run_resource_validation_job(resource_id):
19+
def run_resource_validation_job(resource_id, job_id):
2120
"""
22-
Step 5:
23-
execute validation in the background job and ensure the resource
21+
Execute validation in the background job and ensure the resource
2422
is updated with a final result, including the error case.
2523
"""
26-
log.info("Starting background validation for resource %s", resource_id)
24+
log.info(
25+
"Starting background validation for resource %s (job_id=%s)",
26+
resource_id,
27+
job_id,
28+
)
2729

2830
site_user = toolkit.get_action("get_site_user")({"ignore_auth": True}, {})
2931
context = {"ignore_auth": True, "user": site_user["name"]}
3032

31-
ValidationJob.create(resource_id=resource_id, status=JobStatus.RUNNING)
33+
try:
34+
ValidationJob.update_by_id(job_id, JobStatus.RUNNING)
35+
except ValueError:
36+
log.error("Job record with id %s not found, creating new job record for resource %s", job_id, resource_id)
37+
return
38+
3239
try:
3340
toolkit.get_action("resource_validate")(
3441
context,
3542
{"id": resource_id},
3643
)
37-
log.info("Finished background validation for resource %s", resource_id)
38-
ValidationJob.update(resource_id=resource_id, status=JobStatus.FINISHED)
39-
40-
except ValueError as exc:
41-
log.error(
42-
"No existing validation job found for resource %s when trying to update status: %s",
44+
log.info(
45+
"Finished background validation for resource %s (job_id=%s)",
4346
resource_id,
44-
str(exc),
47+
job_id,
4548
)
46-
ValidationJob.update(resource_id=resource_id, status=JobStatus.ERROR)
49+
ValidationJob.update_by_id(job_id, JobStatus.FINISHED)
4750

4851
except Exception:
4952
log.exception(
50-
"Background validation failed for resource %s",
53+
"Background validation failed for resource %s (job_id=%s)",
5154
resource_id,
55+
job_id,
5256
)
53-
54-
ValidationJob.create(resource_id=resource_id, status=JobStatus.ERROR)
57+
ValidationJob.update_by_id(job_id, JobStatus.ERROR)

ckanext/validate/model/validation_jobs.py

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,25 @@ class JobStatus(str, enum.Enum):
2929

3030
@classmethod
3131
def pending_statuses(cls):
32-
return {cls.QUEUED, cls.STARTED, cls.DEFERRED, cls.SCHEDULED, cls.RUNNING}
32+
return {cls.QUEUED, cls.STARTED, cls.DEFERRED, cls.SCHEDULED}
33+
34+
@classmethod
35+
def running_statuses(cls):
36+
return {cls.RUNNING}
3337

3438
@classmethod
3539
def error_statuses(cls):
36-
return {cls.FAILED, cls.STOPPED, cls.CANCELED}
40+
return {cls.FAILED, cls.STOPPED, cls.CANCELED, cls.ERROR}
41+
42+
@classmethod
43+
def terminal_statuses(cls):
44+
return {
45+
cls.FINISHED,
46+
cls.FAILED,
47+
cls.STOPPED,
48+
cls.CANCELED,
49+
cls.ERROR,
50+
}
3751

3852

3953
class ValidationJob(toolkit.BaseModel, ActiveRecordMixin):
@@ -61,23 +75,40 @@ def __repr__(self):
6175
def create(cls, resource_id, status):
6276
record = cls(
6377
resource_id=resource_id,
64-
status=status,
78+
status=status.value if isinstance(status, JobStatus) else status,
6579
)
6680
record.save()
6781
return record
6882

83+
@classmethod
84+
def get(cls, job_id):
85+
return Session.query(cls).filter(cls.id == job_id).first()
86+
6987
@classmethod
7088
def update(cls, resource_id, status):
7189
record = cls.get_latest_job_for_resource(resource_id)
72-
if record:
73-
record.status = status
74-
if status in (JobStatus.FINISHED, JobStatus.ERROR):
75-
record.finish_timestamp = datetime.datetime.utcnow()
76-
record.commit()
77-
log.info("ValidationJob for resource_id %s updated to status %s", resource_id, status)
78-
return record
79-
else:
90+
if not record:
8091
raise ValueError(f"No existing job found for resource_id {resource_id}")
92+
return cls.update_by_id(record.id, status)
93+
94+
@classmethod
95+
def update_by_id(cls, job_id, status):
96+
record = cls.get(job_id)
97+
if not record:
98+
raise ValueError(f"No existing job found for job_id {job_id}")
99+
100+
record.status = status.value if isinstance(status, JobStatus) else status
101+
if status in JobStatus.terminal_statuses():
102+
record.finish_timestamp = datetime.datetime.utcnow()
103+
104+
record.commit()
105+
log.info(
106+
"ValidationJob id=%s for resource_id=%s updated to status=%s",
107+
record.id,
108+
record.resource_id,
109+
status,
110+
)
111+
return record
81112

82113
@classmethod
83114
def get_latest_job_for_resource(cls, resource_id):

ckanext/validate/resource_hooks.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import ckan.plugins.toolkit as toolkit
44

55
from ckanext.validate import jobs
6+
from ckanext.validate.model.validation_jobs import JobStatus, ValidationJob
67

78
log = logging.getLogger(__name__)
89

@@ -28,20 +29,31 @@ def is_resource_eligible_for_auto_validation(resource_dict):
2829
return True
2930

3031

31-
def build_validation_job_id(resource_id):
32-
return f"validate-resource-{resource_id}"
32+
def enqueue_resource_validation_job(resource_id):
33+
latest_status = ValidationJob.get_latest_job_status_for_resource(resource_id)
3334

35+
# TODO: handel running jobs scenario
36+
37+
if latest_status in JobStatus.pending_statuses():
38+
log.debug(
39+
"Validation job already pending for resource %s (status=%s), skipping enqueue",
40+
resource_id,
41+
latest_status,
42+
)
43+
return None
44+
45+
# Crear el registro ANTES de encolar para que la UI pueda mostrar Pending
46+
job = ValidationJob.create(resource_id=resource_id, status=JobStatus.QUEUED)
3447

35-
def enqueue_resource_validation_job(resource_id):
3648
try:
3749
return toolkit.enqueue_job(
3850
jobs.run_resource_validation_job,
39-
args=[resource_id],
51+
args=[resource_id, job.id],
4052
title=f"Validate resource {resource_id}",
41-
rq_kwargs={"job_id": build_validation_job_id(resource_id)},
4253
)
4354
except Exception:
44-
log.debug("Validation job already enqueued for resource %s, skipping", resource_id)
55+
log.exception("Failed to enqueue validation job for resource %s", resource_id)
56+
ValidationJob.update_by_id(job.id, JobStatus.ERROR)
4557
return None
4658

4759

ckanext/validate/templates/package/resource_read.html

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,12 @@
1717
{% elif status == 'error' %}
1818
<span class="validate-badge validate-badge--error">{{ _('Error') }}</span>
1919
{% elif status == 'pending' %}
20-
<span class="validate-badge validate-badge--pending">{{ _('Pending') }}</span>
20+
<span class="validate-badge validate-badge--pending">
21+
<i class="fa fa-spinner fa-spin"></i>
22+
{{ _('Pending') }}
23+
</span>
24+
{% else %}
25+
<span class="validate-badge validate-badge--pending">{{ _('Not validated') }}</span>
2126
{% endif %}
2227
</a>
2328
</li>

ckanext/validate/templates/package/resource_validate.html

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ <h1>{{ _('Resource Validation') }}</h1>
1818
<h3>{{ _('Current Status') }}</h3>
1919
{% set status = h.get_resource_validation_state(res) %}
2020
{% if status == 'pending' %}
21-
<span class="validate-badge validate-badge--pending">{{ _('Pending') }}</span>
21+
<span class="validate-badge validate-badge--pending">
22+
<i class="fa fa-spinner fa-spin"></i>
23+
{{ _('Pending') }}
24+
</span>
2225
<p class="mt-2">{{ _('Validation is running in the background.') }}</p>
2326

2427
{% elif status == 'success' %}

ckanext/validate/templates/package/snippets/resource_item.html

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@
1111
{% elif status == 'error' %}
1212
<span class="validate-badge validate-badge--error">{{ _('Error') }}</span>
1313
{% elif status == 'pending' %}
14-
<span class="validate-badge validate-badge--pending">{{ _('Pending') }}</span>
14+
<span class="validate-badge validate-badge--pending">
15+
<i class="fa fa-spinner fa-spin"></i>
16+
{{ _('Pending') }}
17+
</span>
18+
{% else %}
19+
<span class="validate-badge validate-badge--pending">{{ _('Not validated') }}</span>
1520
{% endif %}
1621
{% endif %}
1722
{% endblock %}

ckanext/validate/templates/scheming/display_snippets/validation_status.html

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
{# Mostrar badges solo a usuarios logueados y que sean sysadmin o admin #}
22
{% if h.check_access('sysadmin') or h.check_access('package_update', {'id': data['id']}) %}
3-
{% if data[field.field_name] %}
4-
{% set status = data[field.field_name] %}
5-
{% if status == 'success' %}
6-
<span class="validate-badge validate-badge--valid">{{ _('Valid') }}</span>
7-
{% elif status == 'failure' %}
8-
<span class="validate-badge validate-badge--invalid">{{ _('Invalid') }}</span>
9-
{% elif status == 'error' %}
10-
<span class="validate-badge validate-badge--error">{{ _('Error') }}</span>
11-
{% endif %}
3+
{% set status = h.get_resource_validation_state(data) %}
4+
{% if status == 'success' %}
5+
<span class="validate-badge validate-badge--valid">{{ _('Valid') }}</span>
6+
{% elif status == 'failure' %}
7+
<span class="validate-badge validate-badge--invalid">{{ _('Invalid') }}</span>
8+
{% elif status == 'error' %}
9+
<span class="validate-badge validate-badge--error">{{ _('Error') }}</span>
10+
{% elif status == 'pending' %}
11+
<span class="validate-badge validate-badge--pending">
12+
<i class="fa fa-spinner fa-spin"></i>
13+
{{ _('Pending') }}
14+
</span>
1215
{% else %}
1316
<span class="validate-badge validate-badge--pending">{{ _('Not validated') }}</span>
1417
{% endif %}

ckanext/validate/tests/test_jobs.py

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from types import SimpleNamespace
2+
13
from ckanext.validate import jobs
24
from ckanext.validate.model.validation_jobs import JobStatus
35

@@ -27,23 +29,23 @@ def fake_get_action(name):
2729
monkeypatch.setattr(
2830
jobs.ValidationJob,
2931
"create",
30-
lambda resource_id, status: state_calls.append(("create", resource_id, status)),
32+
lambda resource_id, status: SimpleNamespace(id=123),
3133
)
3234
monkeypatch.setattr(
3335
jobs.ValidationJob,
34-
"update",
35-
lambda resource_id, status: state_calls.append(("update", resource_id, status)),
36+
"update_by_id",
37+
lambda job_id, status: state_calls.append(("update_by_id", job_id, status)),
3638
)
3739

38-
jobs.run_resource_validation_job("res-1")
40+
jobs.run_resource_validation_job("res-1", job_id=123)
3941

4042
assert captured == {
4143
"context": {"ignore_auth": True, "user": "site-user"},
4244
"data_dict": {"id": "res-1"},
4345
}
4446
assert state_calls == [
45-
("create", "res-1", JobStatus.RUNNING),
46-
("update", "res-1", JobStatus.FINISHED),
47+
("update_by_id", 123, JobStatus.RUNNING),
48+
("update_by_id", 123, JobStatus.FINISHED),
4749
]
4850

4951

@@ -67,17 +69,51 @@ def fake_get_action(name):
6769
monkeypatch.setattr(
6870
jobs.ValidationJob,
6971
"create",
70-
lambda resource_id, status: state_calls.append(("create", resource_id, status)),
72+
lambda resource_id, status: SimpleNamespace(id=456),
7173
)
7274
monkeypatch.setattr(
7375
jobs.ValidationJob,
74-
"update",
75-
lambda resource_id, status: state_calls.append(("update", resource_id, status)),
76+
"update_by_id",
77+
lambda job_id, status: state_calls.append(("update_by_id", job_id, status)),
7678
)
7779

78-
jobs.run_resource_validation_job("res-2")
80+
jobs.run_resource_validation_job("res-2", job_id=456)
81+
82+
assert state_calls == [
83+
("update_by_id", 456, JobStatus.RUNNING),
84+
("update_by_id", 456, JobStatus.ERROR),
85+
]
86+
87+
88+
def test_run_resource_validation_job_returns_early_when_job_record_does_not_exist(monkeypatch):
89+
resource_validate_called = []
90+
state_calls = []
91+
92+
def fake_get_site_user(context, data_dict):
93+
return {"name": "site-user"}
94+
95+
def fake_resource_validate(context, data_dict):
96+
resource_validate_called.append(True)
97+
return {"id": "res-3"}
98+
99+
def fake_get_action(name):
100+
if name == "get_site_user":
101+
return fake_get_site_user
102+
if name == "resource_validate":
103+
return fake_resource_validate
104+
raise AssertionError(f"Unexpected action requested: {name}")
105+
106+
def fake_update_by_id(job_id, status):
107+
state_calls.append(("update_by_id", job_id, status))
108+
if status == JobStatus.RUNNING:
109+
raise ValueError("No existing job found for job_id")
110+
111+
monkeypatch.setattr(jobs.toolkit, "get_action", fake_get_action)
112+
monkeypatch.setattr(jobs.ValidationJob, "update_by_id", fake_update_by_id)
113+
114+
jobs.run_resource_validation_job("res-3", job_id=321)
79115

116+
assert resource_validate_called == []
80117
assert state_calls == [
81-
("create", "res-2", JobStatus.RUNNING),
82-
("create", "res-2", JobStatus.ERROR),
118+
("update_by_id", 321, JobStatus.RUNNING),
83119
]

0 commit comments

Comments
 (0)