Skip to content

Commit af6c823

Browse files
authored
Merge branch 'master' into improved-multilangual
2 parents 4598103 + f50a8e2 commit af6c823

9 files changed

Lines changed: 70 additions & 145 deletions

File tree

.github/workflows/test.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,13 @@ jobs:
6868
pip install -r ckanext-harvest/requirements.txt
6969
git clone https://github.com/ckan/ckanext-scheming
7070
pip install -e ckanext-scheming
71-
pip install git+https://github.com/ckan/ckanext-fluent.git@4e9340a#egg=ckanext-fluent
71+
git clone https://github.com/ckan/ckanext-fluent
72+
pip install -e ckanext-fluent
7273
git clone https://github.com/ckan/ckanext-dataset-series
7374
pip install -e ckanext-dataset-series
7475
- name: Setup extension
7576
run: |
7677
ckan -c test.ini db init
7778
ckan -c test.ini db pending-migrations --apply
7879
- name: Run tests
79-
run: pytest --ckan-ini=test.ini --cov=ckanext.dcat --cov-report=term-missing --cov-append --disable-warnings ckanext/dcat/tests
80+
run: pytest --ckan-ini=test.ini --cov=ckanext.dcat --cov-report=term-missing --cov-append --disable-warnings ckanext/dcat/tests

ckanext/dcat/harvesters/rdf.py

Lines changed: 33 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -210,18 +210,39 @@ def gather_stage(self, harvest_job):
210210
return []
211211

212212
try:
213-
source_dataset = model.Package.get(harvest_job.source.id)
214-
215-
series_ids, series_mapping = self._parse_and_collect(
216-
parser.dataset_series(),
217-
source_dataset,
218-
harvest_job,
219-
guids_in_source,
220-
is_series=True,
221-
collect_series_mapping=True
222-
)
223-
object_ids += series_ids
224-
object_ids += self._parse_and_collect(parser.datasets(series_mapping), source_dataset, harvest_job, guids_in_source, is_series=False)
213+
214+
source_dataset = model.Package.get(harvest_job.source.id)
215+
216+
for dataset in parser.datasets():
217+
if not dataset.get('name'):
218+
dataset['name'] = self._gen_new_name(dataset['title'])
219+
if dataset['name'] in self._names_taken:
220+
suffix = len([i for i in self._names_taken if i.startswith(dataset['name'] + '-')]) + 1
221+
dataset['name'] = '{}-{}'.format(dataset['name'], suffix)
222+
self._names_taken.append(dataset['name'])
223+
224+
# Unless already set by the parser, get the owner organization (if any)
225+
# from the harvest source dataset
226+
if not dataset.get('owner_org'):
227+
if source_dataset.owner_org:
228+
dataset['owner_org'] = source_dataset.owner_org
229+
230+
# Try to get a unique identifier for the harvested dataset
231+
guid = self._get_guid(dataset, source_url=source_dataset.url)
232+
233+
if not guid:
234+
self._save_gather_error('Could not get a unique identifier for dataset: {0}'.format(dataset),
235+
harvest_job)
236+
continue
237+
238+
dataset['extras'].append({'key': 'guid', 'value': guid})
239+
guids_in_source.append(guid)
240+
241+
obj = HarvestObject(guid=guid, job=harvest_job,
242+
content=json.dumps(dataset))
243+
244+
obj.save()
245+
object_ids.append(obj.id)
225246
except Exception as e:
226247
self._save_gather_error('Error when processsing dataset: %r / %s' % (e, traceback.format_exc()),
227248
harvest_job)
@@ -401,70 +422,3 @@ def import_stage(self, harvest_object):
401422
model.Session.commit()
402423

403424
return True
404-
405-
def _parse_and_collect(
406-
self,
407-
items,
408-
source_dataset,
409-
harvest_job,
410-
guids_in_source,
411-
is_series=False,
412-
collect_series_mapping=False
413-
):
414-
object_ids = []
415-
label = "dataset series" if is_series else "dataset"
416-
series_mapping = {} if collect_series_mapping else None
417-
418-
for item in items:
419-
original_title = item.get("title", label)
420-
if not item.get("name"):
421-
item["name"] = self._gen_new_name(original_title)
422-
423-
if item["name"] in self._names_taken:
424-
suffix = len([i for i in self._names_taken if i.startswith(item["name"] + "-")]) + 1
425-
item["name"] = f"{item['name']}-{suffix}"
426-
427-
self._names_taken.append(item["name"])
428-
429-
if not item.get("owner_org") and source_dataset.owner_org:
430-
item["owner_org"] = source_dataset.owner_org
431-
432-
guid = self._get_guid(item, source_url=source_dataset.url)
433-
if not guid:
434-
self._save_gather_error(f"Could not get a unique identifier for {label}: {item}", harvest_job)
435-
continue
436-
437-
item.setdefault("extras", []).append({"key": "guid", "value": guid})
438-
guids_in_source.append(guid)
439-
440-
obj = HarvestObject(guid=guid, job=harvest_job, content=json.dumps(item))
441-
obj.save()
442-
object_ids.append(obj.id)
443-
444-
# Store mapping of RDF URI to dataset name if requested
445-
if collect_series_mapping:
446-
series_uri = item.get("uri") or item.get("identifier")
447-
if series_uri:
448-
# Try to find an existing active dataset series by 'guid' match
449-
existing = model.Session.query(model.Package).\
450-
join(model.PackageExtra).\
451-
filter(model.PackageExtra.key == 'guid').\
452-
filter(model.PackageExtra.value == series_uri).\
453-
filter(model.Package.type == 'dataset_series').\
454-
filter(model.Package.state == 'active').\
455-
first()
456-
457-
if existing:
458-
item["name"] = existing.name
459-
460-
series_mapping[str(series_uri)] = {
461-
"id": existing.id if existing else item.get("id"),
462-
"name": item["name"]
463-
}
464-
465-
466-
if collect_series_mapping:
467-
return object_ids, series_mapping
468-
469-
return object_ids
470-

ckanext/dcat/helpers.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ def structured_data(dataset_dict, profiles=None):
7272
return _get_serialization(dataset_dict, profiles, "jsonld")
7373

7474

75-
def croissant(dataset_dict, profiles=None):
75+
def croissant(dataset_dict, profiles=None, jsonld_context=None):
7676
"""
7777
Returns a string containing the Croissant ML representation of the given
7878
dataset using the `croissant` profile.
@@ -82,8 +82,10 @@ def croissant(dataset_dict, profiles=None):
8282
if not profiles:
8383
profiles = config.get("ckanext.dcat.croissant.profiles", ["croissant"])
8484

85-
frame = {"@context": JSONLD_CONTEXT, "@type": "sc:Dataset"}
85+
context = jsonld_context or JSONLD_CONTEXT
86+
87+
frame = {"@context": context, "@type": "sc:Dataset"}
8688

8789
return _get_serialization(
88-
dataset_dict, profiles, "jsonld", context=JSONLD_CONTEXT, frame=frame
90+
dataset_dict, profiles, "jsonld", context=context, frame=frame
8991
)

ckanext/dcat/processors.py

Lines changed: 1 addition & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -119,16 +119,6 @@ def _datasets(self):
119119
for dataset in self.g.subjects(RDF.type, DCAT.Dataset):
120120
yield dataset
121121

122-
def _dataset_series(self):
123-
'''
124-
Generator that returns all DCAT dataset series on the graph
125-
126-
Yields rdflib.term.URIRef objects that can be used on graph lookups
127-
and queries
128-
'''
129-
for dataset_series in self.g.subjects(RDF.type, DCAT.DatasetSeries):
130-
yield dataset_series
131-
132122
def next_page(self):
133123
'''
134124
Returns the URL of the next page or None if there is no next page
@@ -183,7 +173,7 @@ def supported_formats(self):
183173
for plugin
184174
in rdflib.plugin.plugins(kind=rdflib.parser.Parser)])
185175

186-
def datasets(self, series_mapping=None):
176+
def datasets(self):
187177
'''
188178
Generator that returns CKAN datasets parsed from the RDF graph
189179
@@ -203,39 +193,6 @@ def datasets(self, series_mapping=None):
203193
)
204194
profile.parse_dataset(dataset_dict, dataset_ref)
205195

206-
# Add in_series if present in RDF and mapped
207-
in_series = []
208-
for series_ref in self.g.objects(dataset_ref, DCAT.inSeries):
209-
key = str(series_ref)
210-
if series_mapping and key in series_mapping:
211-
in_series.append(series_mapping[key]["id"])
212-
213-
if in_series:
214-
dataset_dict["in_series"] = in_series
215-
216-
yield dataset_dict
217-
218-
219-
def dataset_series(self):
220-
'''
221-
Generator that returns CKAN dataset series parsed from the RDF graph
222-
223-
Each dataset series is passed to all the loaded profiles before being
224-
yielded, so it can be further modified by each one of them.
225-
226-
Returns a dataset series dict that can be passed to eg `package_create`
227-
or `package_update`
228-
'''
229-
for dataset_ref in self._dataset_series():
230-
dataset_dict = {}
231-
for profile_class in self._profiles:
232-
profile = profile_class(
233-
self.g,
234-
dataset_type=self.dataset_type,
235-
compatibility_mode=self.compatibility_mode
236-
)
237-
profile.parse_dataset(dataset_dict, dataset_ref)
238-
239196
yield dataset_dict
240197

241198

ckanext/dcat/profiles/croissant.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
JSONLD_CONTEXT = {
2626
"@vocab": "https://schema.org/",
27+
"@language": config.get("ckan.locale_default"),
2728
"sc": "https://schema.org/",
2829
"cr": "http://mlcommons.org/croissant/",
2930
"rai": "http://mlcommons.org/croissant/RAI/",

ckanext/dcat/profiles/euro_dcat_ap_3.py

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,6 @@ def parse_dataset(self, dataset_dict, dataset_ref):
3030
# DCAT AP v2 scheming fields
3131
dataset_dict = self._parse_dataset_v2_scheming(dataset_dict, dataset_ref)
3232

33-
34-
# Check if it's a dataset series
35-
if (dataset_ref, RDF.type, DCAT.DatasetSeries) in self.g:
36-
dataset_dict["type"] = "dataset_series"
37-
38-
# Example defaulting logic (adjust based on RDF vocab if you have it)
39-
if "series_order_field" not in dataset_dict:
40-
dataset_dict["series_order_field"] = "metadata_created"
41-
if "series_order_type" not in dataset_dict:
42-
dataset_dict["series_order_type"] = "date"
43-
4433
# DCAT AP v3: hasVersion
4534
values = self._object_value_list(dataset_ref, DCAT.hasVersion)
4635
if values:

ckanext/dcat/profiles/euro_health_dcat_ap.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ def _parse_retention_period(self, subject_ref):
183183

184184
return [retention_dict] if retention_dict else []
185185

186+
186187
def graph_from_dataset(self, dataset_dict, dataset_ref):
187188
super().graph_from_dataset(dataset_dict, dataset_ref)
188189
for prefix, namespace in namespaces.items():

ckanext/dcat/schemas/health_dcat_ap.yaml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -733,8 +733,6 @@ resource_fields:
733733

734734
- field_name: rights
735735
label: Rights
736-
form_snippet: markdown.html
737-
display_snippet: markdown.html
738736
preset: multiple_text
739737
validators: ignore_missing scheming_multiple_text
740738

ckanext/dcat/tests/test_blueprints.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,28 @@ def test_dataset_default(self, app):
6666
assert dcat_dataset['title'] == dataset['title']
6767
assert dcat_dataset['notes'] == dataset['notes']
6868

69+
def test_dataset_default_private(self, app):
70+
user = factories.UserWithToken()
71+
org = factories.Organization(users=[{"name": user["name"], "capacity": "admin"}])
72+
dataset = factories.Dataset(
73+
notes='Test dataset',
74+
owner_org=org['id'],
75+
private=True
76+
)
77+
78+
url = url_for('dcat.read_dataset', _id=dataset['name'], _format='rdf')
79+
80+
81+
# Unauthenticated request
82+
response = app.get(url)
83+
assert response.status_code == 403
84+
85+
# Authenticated request
86+
headers = {"Authorization": user["token"]}
87+
response = app.get(url, headers=headers)
88+
89+
assert response.headers['Content-Type'] == 'application/rdf+xml'
90+
6991
def test_dataset_xml(self, app):
7092

7193
dataset = factories.Dataset(
@@ -612,8 +634,8 @@ def test_croissant_metadata_embedded(self, app):
612634
response = app.get(url)
613635

614636
assert '<script type="application/ld+json">' in response.body
615-
assert '"description": "test description"' in response.body
616-
assert '"conformsTo": "http://mlcommons.org/croissant/1.0"' in response.body
637+
assert '"@value": "test description"' in response.body
638+
assert '"@value": "http://mlcommons.org/croissant/1.0"' in response.body
617639

618640
@pytest.mark.ckan_config('ckan.plugins', 'dcat croissant')
619641
def test_croissant_metadata_endpoint(self, app):
@@ -627,8 +649,8 @@ def test_croissant_metadata_endpoint(self, app):
627649
response = app.get(url)
628650
croissant_dict = json.loads(response.body)
629651

630-
assert croissant_dict["description"] == "test description"
631-
assert croissant_dict["conformsTo"] == "http://mlcommons.org/croissant/1.0"
652+
assert croissant_dict["description"] == {"@value": "test description"}
653+
assert croissant_dict["conformsTo"] == {"@value": "http://mlcommons.org/croissant/1.0"}
632654

633655

634656
@pytest.mark.usefixtures("with_plugins", "clean_db", "clean_index")

0 commit comments

Comments
 (0)