Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 141 additions & 1 deletion ckanext/validate/assets/css/validate.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* === Badges de estado de validación (global) === */
.validate-badge {
display: inline-block;
padding: 0.25em 0.6em;
Expand Down Expand Up @@ -46,6 +47,7 @@
}


/* === Página de validación de recurso (resource_validate.html) === */
.validate-page {
max-width: 980px;
}
Expand Down Expand Up @@ -282,4 +284,142 @@
.validate-rules-details__content ul {
margin-bottom: 10px;
padding-left: 20px;
}
}

/* === Página de estadísticas de validación (validation_statistics.html) === */
.validation-chart-card {
margin-bottom: 24px;
padding: 20px;
border: 1px solid #ddd;
border-radius: 4px;
background: #fff;
}

.validation-chart-card h2 {
margin-top: 0;
}

.validation-status-chart {
display: flex;
width: 100%;
height: 34px;
margin: 15px 0 10px;
overflow: hidden;
border-radius: 4px;
background: #eee;
}

.validation-status-chart__valid {
background: #2e8540;
}

.validation-status-chart__invalid {
background: #d9534f;
}

.validation-status-chart__other {
background: #777;
}

.validation-chart-legend {
display: flex;
flex-wrap: wrap;
gap: 18px;
margin: 0;
padding: 0;
list-style: none;
}

.validation-chart-legend__marker {
display: inline-block;
width: 12px;
height: 12px;
margin-right: 5px;
border-radius: 2px;
vertical-align: -1px;
}

.validation-chart-legend__marker--valid {
background: #2e8540;
}

.validation-chart-legend__marker--invalid {
background: #d9534f;
}

.validation-chart-legend__marker--other {
background: #777;
}

.validation-horizontal-chart {
margin: 0;
padding: 0;
list-style: none;
}

.validation-horizontal-chart__item {
display: grid;
grid-template-columns: minmax(160px, 1fr) 3fr 70px;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}

.validation-horizontal-chart__track {
height: 18px;
overflow: hidden;
border-radius: 3px;
background: #eee;
}

.validation-horizontal-chart__bar {
min-width: 3px;
height: 100%;
background: #337ab7;
}

.validation-timeline {
display: flex;
align-items: flex-end;
gap: 7px;
min-height: 230px;
padding: 15px 10px 0;
overflow-x: auto;
border-bottom: 1px solid #bbb;
}

.validation-timeline__item {
display: flex;
flex: 1 0 28px;
flex-direction: column;
justify-content: flex-end;
min-width: 28px;
height: 210px;
text-align: center;
}

.validation-timeline__value {
margin-bottom: 4px;
font-size: 11px;
}

.validation-timeline__bar {
min-height: 2px;
border-radius: 3px 3px 0 0;
background: #337ab7;
}

.validation-timeline__label {
margin-top: 6px;
font-size: 10px;
white-space: nowrap;
transform: rotate(-45deg);
transform-origin: top center;
}

@media (max-width: 767px) {
.validation-horizontal-chart__item {
grid-template-columns: 1fr;
gap: 4px;
}
}
100 changes: 100 additions & 0 deletions ckanext/validate/blueprints/admin.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import logging
from datetime import datetime, timezone

from flask import Blueprint

from ckan.lib import base
from ckan.plugins import toolkit

from ckanext.validate.model.validation import Validation
from ckanext.validate.model.validation_jobs import JobStatus, ValidationJob
from ckanext.validate.helpers import format_timestamp_for_display

Expand Down Expand Up @@ -84,3 +86,101 @@ def _enrich(job_dict):
"selected_status": selected_status,
},
)


VALIDATION_STATISTICS_PERIODS = {
"1_month": "1 month",
"6_months": "6 months",
"1_year": "1 year",
}


@validation_jobs_blueprint.route(
"/ckan-admin/validation-statistics",
methods=["GET"],
)
def validation_statistics():
"""Display validation statistics for a predefined reporting period."""
context = {"user": toolkit.current_user.name}

try:
toolkit.check_access("sysadmin", context)
except toolkit.NotAuthorized:
base.abort(
403,
toolkit._("Need to be system administrator to administer"),
)

selected_period = toolkit.request.args.get(
"period",
"1_month",
).strip()

if selected_period not in VALIDATION_STATISTICS_PERIODS:
toolkit.h.flash_error(
toolkit._("Invalid statistics period: {0}").format(
selected_period
)
)
selected_period = "1_month"

report_end = datetime.now(timezone.utc)
validations = Validation.get_by_period(
selected_period,
end_date=report_end,
)
summary = Validation.get_statistics_summary(validations)
errors_by_type = Validation.group_errors_by_type(validations)
resources = Validation.group_errors_by_resource(validations)
timeline = Validation.get_statistics_timeline(
validations,
selected_period,
end_date=report_end,
)
max_error_type_count = max(
(item["count"] for item in errors_by_type),
default=1,
)
max_timeline_error_count = max(
(item["error_count"] for item in timeline),
default=1,
)

resource_show = toolkit.get_action("resource_show")

for resource_summary in resources:
resource_summary["resource_name"] = resource_summary["resource_id"]
resource_summary["resource_url"] = None

try:
resource = resource_show(
context,
{"id": resource_summary["resource_id"]},
)
except (toolkit.ObjectNotFound, toolkit.NotAuthorized):
continue

resource_summary["resource_name"] = (
resource.get("name")
or resource.get("description")
or resource_summary["resource_id"]
)
resource_summary["resource_url"] = toolkit.url_for(
"resource.read",
id=resource["package_id"],
resource_id=resource["id"],
)

return base.render(
"admin/validation_statistics.html",
extra_vars={
"summary": summary,
"errors_by_type": errors_by_type,
"resources": resources,
"periods": VALIDATION_STATISTICS_PERIODS,
"selected_period": selected_period,
"timeline": timeline,
"max_error_type_count": max_error_type_count,
"max_timeline_error_count": max_timeline_error_count,
},
)
Loading
Loading