Skip to content

Commit 054150e

Browse files
committed
fix XLSXHarvester-V1: permite variación en nombres de pestañas. Permite columnas faltantes si son campos opciones. Ataja errores de postgres (ids duplicados)
1 parent 6a1c722 commit 054150e

1 file changed

Lines changed: 69 additions & 11 deletions

File tree

ckanext/harvest/harvesters/xlsx_harvester.py

Lines changed: 69 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
"dataset_description": "notes",
3535
"dataset_publisher_name": "dataset_publisher_name",
3636
"dataset_publisher_mbox": "dataset_publisher_mbox",
37+
"dataset_contactPoint_fn": "dataset_contactPoint_fn",
38+
"dataset_contactPoint_hasEmail": "dataset_contactPoint_hasEmail",
3739
"dataset_superTheme": "dataset_superTheme",
3840
"dataset_theme": "dataset_theme",
3941
"dataset_keyword": "dataset_keywords",
@@ -43,6 +45,7 @@
4345
"dataset_language": "dataset_language",
4446
"dataset_spatial": "spatial",
4547
"dataset_temporal": "temporal_start",
48+
"dataset_landingPage": "dataset_landingPage",
4649
"dataset_license": "license_id",
4750
"dataset_source": "dataset_source",
4851
}
@@ -53,10 +56,14 @@
5356
"distribution_title": "name",
5457
"distribution_description": "description",
5558
"distribution_downloadURL": "url",
59+
"distribution_fileName": "distribution_fileName",
5660
"distribution_format": "format",
5761
"distribution_mediaType": "mimetype",
58-
"distribution_modified":"last_modified",
59-
"distribution_issued" : "created"
62+
"distribution_license": "distribution_license",
63+
"distribution_byteSize": "distribution_byteSize",
64+
"distribution_modified": "last_modified",
65+
"distribution_issued": "created",
66+
"distribution_rights": "distribution_rights",
6067
}
6168

6269
# -----------------------------------------------------------------------------
@@ -99,16 +106,44 @@
99106
"distribution_wfs_url": "wfs_url", # URL servicio WFS
100107
}
101108

102-
DEFAULT_DATASET_SHEET = "dataset"
103-
DEFAULT_DISTRIBUTION_SHEET = "distribution"
109+
DEFAULT_DATASET_SHEET = ["dataset", "Dataset", "datasets", "Datasets"]
110+
DEFAULT_DISTRIBUTION_SHEET = ["distribution", "Distribution", "distributions", "Distributions"]
104111

105-
DATASET_REQUIRED = {}
112+
113+
def _resolve_sheet_name(wb, candidates):
114+
"""Retorna el primer nombre de hoja del workbook que coincida con alguno de los candidatos."""
115+
if isinstance(candidates, str):
116+
candidates = [candidates]
117+
return next((s for s in wb.sheetnames if s in candidates), None)
118+
119+
DATASET_REQUIRED = {"title"}
106120
DISTRIBUTION_REQUIRED = {"url"}
107121

108-
# Columnas del mapping V1 que pueden no estar en el Excel (opcionales)
122+
# Columnas del mapping V1 que pueden no estar en el Excel (opcionales/recomendadas)
109123
# La validación de headers será permisiva para estas.
110-
ANDINO_V1_DATASET_OPTIONAL = set()
111-
ANDINO_V1_RESOURCE_OPTIONAL = set()
124+
ANDINO_V1_DATASET_OPTIONAL = {
125+
"dataset_publisher_mbox",
126+
"dataset_contactPoint_fn",
127+
"dataset_contactPoint_hasEmail",
128+
"dataset_theme",
129+
"dataset_keyword",
130+
"dataset_modified",
131+
"dataset_language",
132+
"dataset_spatial",
133+
"dataset_temporal",
134+
"dataset_landingPage",
135+
"dataset_license",
136+
}
137+
ANDINO_V1_RESOURCE_OPTIONAL = {
138+
"distribution_description",
139+
"distribution_fileName",
140+
"distribution_format",
141+
"distribution_mediaType",
142+
"distribution_license",
143+
"distribution_byteSize",
144+
"distribution_modified",
145+
"distribution_rights",
146+
}
112147

113148

114149
class XLSXHarvester(HarvesterBase):
@@ -246,6 +281,8 @@ def _validate_mapping_headers(self, workbook, sheet_name, mapping, skip=0, optio
246281
"""
247282
optional_keys = {k.lower() for k in (optional_keys or set())}
248283

284+
if sheet_name is None:
285+
raise ValueError("No existe la hoja '%s'" % sheet_name)
249286
try:
250287
ws = workbook[sheet_name]
251288
except KeyError:
@@ -308,9 +345,9 @@ def gather_stage(self, harvest_job):
308345
)
309346
return []
310347

311-
skip = self.config.get("skip_rows", 0)
312-
ds_sheet = self.config.get("dataset_sheet", DEFAULT_DATASET_SHEET)
313-
dist_sheet = self.config.get("distribution_sheet", DEFAULT_DISTRIBUTION_SHEET)
348+
skip = self.config.get("skip_rows", 0)
349+
ds_sheet = _resolve_sheet_name(wb, self.config.get("dataset_sheet", DEFAULT_DATASET_SHEET))
350+
dist_sheet = _resolve_sheet_name(wb, self.config.get("distribution_sheet", DEFAULT_DISTRIBUTION_SHEET))
314351

315352
ds_rows = dist_rows = None
316353
errors = []
@@ -578,6 +615,27 @@ def import_stage(self, harvest_object):
578615
model.Session.commit()
579616
return True
580617

618+
except sa.exc.IntegrityError as e:
619+
model.Session.rollback()
620+
orig = getattr(e, "orig", None)
621+
orig_type = type(orig).__name__ if orig else ""
622+
if orig_type == "UniqueViolation" and "resource_pkey" in str(e):
623+
resources = package_dict.get("resources", [])
624+
ids = [r.get("id") for r in resources if r.get("id")]
625+
dupes = sorted({i for i in ids if ids.count(i) > 1})
626+
self._save_object_error(
627+
"IDs de distribución duplicados en el dataset '%s': %s. "
628+
"Revisá que los distribution_identifier sean únicos en la fuente."
629+
% (package_dict.get("name", "?"), ", ".join(dupes)),
630+
harvest_object,
631+
"Import",
632+
)
633+
else:
634+
self._save_object_error(
635+
"Error de integridad al guardar en CKAN: %s" % str(e),
636+
harvest_object,
637+
"Import",
638+
)
581639
except Exception as e:
582640
model.Session.rollback()
583641
self._save_object_error(

0 commit comments

Comments
 (0)