Skip to content

Commit d36b031

Browse files
committed
[Fixes #13642] Multilang: metadata handler - tests
1 parent a9a3efc commit d36b031

File tree

7 files changed

+290
-7
lines changed

7 files changed

+290
-7
lines changed

geonode/metadata/apps.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ def setup_metadata_handlers():
3434
metadata_manager.add_handler(handler_id, handler)
3535
ids.append(handler_id)
3636

37-
for handler in metadata_manager.handlers.values():
38-
handler.post_init()
37+
metadata_manager.post_init()
3938

4039
logger.info(f"Metadata handlers from config: {', '.join(METADATA_HANDLERS)}")

geonode/metadata/handlers/multilang.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,17 @@
3030

3131
class MultiLangHandler(MetadataHandler):
3232

33+
def __init__(self, **kwargs):
34+
self.sparse_registry = kwargs.get("registry", sparse_field_registry)
35+
3336
def post_init(self):
3437
# register all multilang localized fields as sparse fields
3538
for property_name in settings.MULTILANG_FIELDS:
3639
prev_field = property_name
3740
for lang, ml_property_name in multi.get_multilang_field_names(property_name):
3841
ml_subschema = self._create_ml_subschema(property_name, lang)
3942
logger.debug(f"Registering multilang sparse field {property_name} --> {ml_property_name}")
40-
sparse_field_registry.register(
43+
self.sparse_registry.register(
4144
ml_property_name, ml_subschema, after=prev_field, init_func=self.init_func
4245
)
4346
prev_field = ml_property_name
@@ -61,6 +64,7 @@ def init_func(field_name, subschema, jsonschema, req_lang):
6164
def update_schema(self, jsonschema, context, lang=None):
6265
for property_name in settings.MULTILANG_FIELDS:
6366
# validate constraints
67+
logging.debug(f"Validating multilang field {property_name}")
6468
parent_schema = jsonschema["properties"][property_name]
6569
if not MetadataHandler._check_type(parent_schema["type"], "string"):
6670
raise ValueError(f"Field {property_name} cannot be multilang")
@@ -107,5 +111,4 @@ def pre_deserialization(self, resource, jsonschema: dict, instance: dict, contex
107111
instance[property_name] = def_lang_value
108112

109113
def update_resource(self, resource, field_name, json_instance, context, errors, **kwargs):
110-
# TODO
111114
pass

geonode/metadata/handlers/sparse.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@
3535

3636
class SparseFieldRegistry:
3737

38-
sparse_fields = {}
38+
def __init__(self):
39+
self.sparse_fields = {}
3940

4041
def register(self, field_name: str, schema: dict, after: str = None, init_func=None):
4142
self.sparse_fields[field_name] = {"schema": schema, "after": after, "init_func": init_func}

geonode/metadata/manager.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,13 @@ def __init__(self):
5050
def add_handler(self, handler_id, handler):
5151
self.handlers[handler_id] = handler()
5252

53+
def post_init(self):
54+
"""
55+
To be called once all the handlers have been added to the MetadataManager
56+
"""
57+
for handler in self.handlers.values():
58+
handler.post_init()
59+
5360
def _init_schema_context(self, lang):
5461
return {"labels": self._i18n_cache.get_labels(lang)}
5562

@@ -62,7 +69,6 @@ def build_schema(self, lang=None):
6269
context = self._init_schema_context(lang)
6370

6471
for key, handler in self.handlers.items():
65-
# logger.debug(f"build_schema: update schema -> {key}")
6672
schema = handler.update_schema(schema, context, lang)
6773

6874
# Set required fields.
@@ -112,7 +118,7 @@ def build_schema_instance(self, resource, lang=None):
112118
handler.post_serialization(resource, schema, instance, context)
113119

114120
# TESTING ONLY
115-
if "error" in resource.title.lower():
121+
if resource and "error" in resource.title.lower():
116122
for fieldname in schema["properties"]:
117123
MetadataHandler._set_error(
118124
errors, [fieldname], f"TEST: test msg for field '{fieldname}' in GET request"
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"title": {
3+
"type": "string",
4+
"title": "Title",
5+
"description": "name by which the cited resource is known",
6+
"maxLength": 255,
7+
"geonode:handler": "fake",
8+
"geonode:required": true
9+
},
10+
"abstract": {
11+
"type": "string",
12+
"title": "Abstract",
13+
"description": "brief narrative summary of the content of the resource(s)",
14+
"maxLength": 2000,
15+
"ui:options": {
16+
"widget": "textarea",
17+
"rows": 5
18+
},
19+
"geonode:handler": "fake",
20+
"geonode:required": true
21+
},
22+
"license": {
23+
"type": "object",
24+
"title": "License",
25+
"description": "license of the dataset",
26+
"maxLength": 255,
27+
"properties": {
28+
"id": {
29+
"type": "string"
30+
},
31+
"label": {
32+
"type": "string"
33+
}
34+
},
35+
"geonode:handler": "fake"
36+
}
37+
}

geonode/metadata/tests/handlers.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#########################################################################
2+
#
3+
# Copyright (C) 2024 OSGeo
4+
#
5+
# This program is free software: you can redistribute it and/or modify
6+
# it under the terms of the GNU General Public License as published by
7+
# the Free Software Foundation, either version 3 of the License, or
8+
# (at your option) any later version.
9+
#
10+
# This program is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
# GNU General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU General Public License
16+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
#
18+
#########################################################################
19+
20+
import json
21+
import logging
22+
23+
from geonode.metadata.handlers.abstract import MetadataHandler
24+
25+
26+
logger = logging.getLogger(__name__)
27+
28+
29+
class LoaderHandler(MetadataHandler):
30+
"""
31+
This handler simply loads a json file and adds fields from it.
32+
"""
33+
34+
def __init__(self, **kwargs):
35+
self.schema_file = kwargs.get("schemafile")
36+
self.base_schema = None
37+
38+
def update_schema(self, jsonschema, context, lang=None):
39+
40+
if not self.base_schema:
41+
with open(self.schema_file) as f:
42+
self.base_schema = json.load(f)
43+
44+
for property_name, subschema in self.base_schema.items():
45+
self._add_subschema(jsonschema, property_name, subschema)
46+
47+
if "geonode:handler" not in subschema:
48+
raise KeyError(f"Missing schema handler for property {property_name}")
49+
50+
return jsonschema
51+
52+
def get_jsonschema_instance(self, resource, field_name, context, errors, lang=None):
53+
raise KeyError(f"Unexpected get request for field {field_name}")
54+
55+
def update_resource(self, resource, field_name, json_instance, context, errors, **kwargs):
56+
raise KeyError(f"Unexpected update request for field {field_name}")
57+
58+
59+
class FakeHandler(MetadataHandler):
60+
"""
61+
This handler
62+
- does not add any subschema
63+
- swallow any update request
64+
- create fake string values for instances
65+
"""
66+
67+
def update_schema(self, jsonschema, context, lang=None):
68+
return jsonschema
69+
70+
def get_jsonschema_instance(self, resource, field_name, context, errors, lang=None):
71+
return f"{field_name}_fake"
72+
73+
def update_resource(self, resource, field_name, json_instance, context, errors, **kwargs):
74+
pass
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
#########################################################################
2+
#
3+
# Copyright (C) 2024 OSGeo
4+
#
5+
# This program is free software: you can redistribute it and/or modify
6+
# it under the terms of the GNU General Public License as published by
7+
# the Free Software Foundation, either version 3 of the License, or
8+
# (at your option) any later version.
9+
#
10+
# This program is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
# GNU General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU General Public License
16+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
#
18+
#########################################################################
19+
20+
import os
21+
import logging
22+
from types import SimpleNamespace
23+
24+
from unittest.mock import patch, MagicMock
25+
26+
from django.test import override_settings
27+
28+
from geonode.base.models import ResourceBase
29+
from geonode.metadata.handlers.multilang import MultiLangHandler
30+
from geonode.metadata.handlers.sparse import SparseHandler, SparseFieldRegistry
31+
from geonode.metadata.manager import MetadataManager
32+
from geonode.metadata.multilang import utils as multi
33+
from geonode.metadata.tests.handlers import FakeHandler, LoaderHandler
34+
35+
from geonode.tests.base import GeoNodeBaseTestSupport
36+
37+
38+
logger = logging.getLogger(__name__)
39+
40+
41+
class MetadataMultilangTests(GeoNodeBaseTestSupport):
42+
43+
def setUp(self):
44+
pass
45+
46+
def tearDown(self):
47+
super().tearDown()
48+
49+
def create_metadata_manager(self):
50+
sr = SparseFieldRegistry()
51+
mm = MetadataManager()
52+
mm.handlers = {
53+
# "base": BaseHandler(),
54+
"loader": LoaderHandler(schemafile=os.path.join(os.path.dirname(__file__), "data/minimal_schema.json")),
55+
"fake": FakeHandler(),
56+
"sparse": SparseHandler(registry=sr),
57+
"multilang": MultiLangHandler(registry=sr),
58+
}
59+
mm.post_init()
60+
return mm, sr
61+
62+
def test_schema_add_language(self):
63+
"""
64+
The MultiLang handler should create one field for each language for each field declared as multilang
65+
"""
66+
67+
with(override_settings(
68+
LANGUAGES=[("en", "English"), ("it", "Italiano")],
69+
MULTILANG_FIELDS=())
70+
):
71+
mm, sr = self.create_metadata_manager()
72+
schema = mm.build_schema()
73+
start_len = len(schema["properties"])
74+
75+
self.assertIn("title", schema["properties"])
76+
self.assertEqual(0, len(sr.fields()), f"Unexpected sparse fields found: {sr.fields()}")
77+
78+
with(override_settings(
79+
LANGUAGES=[("en", "English"), ("it", "Italiano")],
80+
MULTILANG_FIELDS=["title"])
81+
):
82+
mm, sr = self.create_metadata_manager()
83+
schema = mm.build_schema()
84+
85+
self.assertEqual(2, len(sr.fields()), "Bad set of sparse fields found")
86+
self.assertEqual(start_len + 2, len(schema["properties"]), "Bad number of properties in schema")
87+
self.assertIn(multi.get_multilang_field_name("title", "en"), schema["properties"], "Multilang field not found")
88+
89+
def test_bad_field(self):
90+
"""
91+
Only text fields should be declared as multilang
92+
"""
93+
with(override_settings(
94+
LANGUAGES=[("en", "English"), ("it", "Italiano")],
95+
MULTILANG_FIELDS=["license"])
96+
):
97+
try:
98+
mm, _ = self.create_metadata_manager()
99+
mm.build_schema()
100+
self.fail("Non multilang-able field not detected")
101+
except ValueError as e :
102+
logger.info(f"Bad multilang field properly caught: {e}")
103+
104+
def test_unexisting_field(self):
105+
"""
106+
Test field existence on declaration of multilang field
107+
"""
108+
with(override_settings(
109+
LANGUAGES=[("en", "English"), ("it", "Italiano")],
110+
MULTILANG_FIELDS=["does_not_exists"])
111+
):
112+
try:
113+
mm, _ = self.create_metadata_manager()
114+
mm.build_schema()
115+
self.fail("Missing field not detected")
116+
except KeyError as e :
117+
logger.info(f"Missing field properly caught: {e}")
118+
119+
def test_default_preserialization(self):
120+
"""
121+
Check that if a multilang field is not populated but the main field is,
122+
the main field value is copied into the multilang field of the default language
123+
"""
124+
with(override_settings(
125+
LANGUAGE_CODE="it",
126+
LANGUAGES=[("en", "English"), ("it", "Italiano")],
127+
MULTILANG_FIELDS=["title"])
128+
):
129+
mm, _ = self.create_metadata_manager()
130+
schema = mm.get_schema()
131+
instance = mm.build_schema_instance(None)
132+
# logger.info(f"INSTANCE {instance}")
133+
self.assertEqual("title_fake", instance["title"])
134+
self.assertEqual("title_fake", instance["title_multilang_it"])
135+
self.assertIsNone(instance["title_multilang_en"])
136+
137+
@patch("geonode.base.models.ResourceBase.get_real_instance_class")
138+
@patch("geonode.indexing.manager.TSVectorIndexManager.update_index")
139+
@patch("geonode.metadata.handlers.sparse.SparseHandler.update_resource")
140+
def test_default_base_value(self, mock_get_real_instance_class, mock_update_index, mock_sparse_update):
141+
"""
142+
The value in the default language of the multilang field should go in the base field whan saving
143+
"""
144+
with(override_settings(
145+
LANGUAGE_CODE="it",
146+
LANGUAGES=[("en", "English"), ("it", "Italiano")],
147+
MULTILANG_FIELDS=["title"])
148+
):
149+
mm, _ = self.create_metadata_manager()
150+
instance = {
151+
'title': 'whatever',
152+
'title_multilang_it': 'title_it',
153+
'title_multilang_en': None,
154+
'abstract': 'abstract_fake',
155+
'license': 'license_fake'
156+
}
157+
158+
resource = ResourceBase()
159+
fake_req = SimpleNamespace(data=instance, user=None)
160+
mm.update_schema_instance(resource, fake_req)
161+
logger.info(f"INSTANCE POST {instance}")
162+
163+
self.assertEqual("title_it", instance["title"])

0 commit comments

Comments
 (0)