Skip to content

Commit 54e2b5b

Browse files
committed
[Fixes #13642] Multilang: metadata handler - tests
1 parent 170c965 commit 54e2b5b

File tree

9 files changed

+295
-9
lines changed

9 files changed

+295
-9
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")
@@ -109,5 +113,4 @@ def pre_deserialization(self, resource, jsonschema: dict, instance: dict, partia
109113
partial.add(property_name)
110114

111115
def update_resource(self, resource, field_name, json_instance, context, errors, **kwargs):
112-
# TODO
113116
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

geonode/metadata/tests/test_handlers.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#
1818
#########################################################################
1919

20+
import copy
2021
import os
2122
import json
2223
from unittest.mock import patch, MagicMock
@@ -72,7 +73,7 @@ class HandlersTests(GeoNodeBaseTestSupport):
7273

7374
def setUp(self):
7475
# set Json schemas
75-
self.model_schema = MODEL_SCHEMA
76+
self.model_schema = copy.deepcopy(MODEL_SCHEMA)
7677
self.lang = None
7778
self.errors = {}
7879
self.context = MagicMock()
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
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
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(
88+
multi.get_multilang_field_name("title", "en"), schema["properties"], "Multilang field not found"
89+
)
90+
91+
def test_bad_field(self):
92+
"""
93+
Only text fields should be declared as multilang
94+
"""
95+
with override_settings(
96+
LANGUAGES=[("en", "English"), ("it", "Italiano")],
97+
MULTILANG_FIELDS=["license"],
98+
):
99+
try:
100+
mm, _ = self.create_metadata_manager()
101+
mm.build_schema()
102+
self.fail("Non multilang-able field not detected")
103+
except ValueError as e:
104+
logger.info(f"Bad multilang field properly caught: {e}")
105+
106+
def test_unexisting_field(self):
107+
"""
108+
Test field existence on declaration of multilang field
109+
"""
110+
with override_settings(
111+
LANGUAGES=[("en", "English"), ("it", "Italiano")],
112+
MULTILANG_FIELDS=["does_not_exists"],
113+
):
114+
try:
115+
mm, _ = self.create_metadata_manager()
116+
mm.build_schema()
117+
self.fail("Missing field not detected")
118+
except KeyError as e:
119+
logger.info(f"Missing field properly caught: {e}")
120+
121+
def test_default_preserialization(self):
122+
"""
123+
Check that if a multilang field is not populated but the main field is,
124+
the main field value is copied into the multilang field of the default language
125+
"""
126+
with override_settings(
127+
LANGUAGE_CODE="it",
128+
LANGUAGES=[("en", "English"), ("it", "Italiano")],
129+
MULTILANG_FIELDS=["title"],
130+
):
131+
mm, _ = self.create_metadata_manager()
132+
instance = mm.build_schema_instance(None)
133+
# logger.info(f"INSTANCE {instance}")
134+
self.assertEqual("title_fake", instance["title"])
135+
self.assertEqual("title_fake", instance["title_multilang_it"])
136+
self.assertIsNone(instance["title_multilang_en"])
137+
138+
@patch("geonode.base.models.ResourceBase.get_real_instance_class")
139+
@patch("geonode.indexing.manager.TSVectorIndexManager.update_index")
140+
@patch("geonode.metadata.handlers.sparse.SparseHandler.update_resource")
141+
def test_default_base_value(self, mock_get_real_instance_class, mock_update_index, mock_sparse_update):
142+
"""
143+
The value in the default language of the multilang field should go in the base field whan saving
144+
"""
145+
with override_settings(
146+
LANGUAGE_CODE="it",
147+
LANGUAGES=[("en", "English"), ("it", "Italiano")],
148+
MULTILANG_FIELDS=["title"],
149+
):
150+
mm, _ = self.create_metadata_manager()
151+
self.assertNotIn("base", mm.handlers)
152+
instance = {
153+
"title": "whatever",
154+
"title_multilang_it": "title_it",
155+
"title_multilang_en": None,
156+
"abstract": "abstract_fake",
157+
"license": "license_fake",
158+
}
159+
160+
resource = ResourceBase()
161+
fake_req = SimpleNamespace(data=instance, user=None)
162+
mm.update_schema_instance(resource, fake_req)
163+
164+
self.assertEqual("title_it", instance["title"])

geonode/metadata/tests/tests.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#
1818
#########################################################################
1919

20+
import copy
2021
import os
2122
import json
2223
from unittest.mock import patch, MagicMock, ANY
@@ -58,7 +59,7 @@ class MetadataApiTests(APITestCase):
5859

5960
def setUp(self):
6061
# set Json schemas
61-
self.model_schema = MODEL_SCHEMA
62+
self.model_schema = copy.deepcopy(MODEL_SCHEMA)
6263
self.lang = None
6364

6465
self.test_user_1 = get_user_model().objects.create_user(

0 commit comments

Comments
 (0)