Repository: camptocamp/python-geoservercloud

Issue:
Handle default value of i18n attribute on PUT
When doing a PUT on an object with an i18n attribute (for instance `internationalTitle` in a feature type) it seems GeoServer will takes the first of the translation values and set it as the non-i18ned value (for instance `title`). This value than acts as a default value in the WebUI and when doing WMS requests without specifying a language. It is a problem when:
- updating the `internationalTitle`, because GeoServer does not update the `title` accordingly so the old value is still there
- copying a layer, because the lib until now always assumed the two attributes were mutually exclusive, so only the `title` is copied and the translations are lost.

See example below:
```
>>> import requests
>>> requests.post(
...     "http://localhost:9099/geoserver/rest/workspaces/test/datastores/test/featuretypes.json",
...     json={
...         "featureType": {
...             "name": "situation",
...             "nativeName": "situation",
...             "internationalTitle": {"fr": "Titre de la couche", "en": "Layer title"},
...         }
...     },
...     auth=("admin", "geoserver"),
... )
<Response [201]>
>>> response = requests.get(
...     "http://localhost:9099/geoserver/rest/workspaces/test/datastores/test/featuretypes/situation.json",
...     auth=("admin", "geoserver"),
... )
>>> print(response.json().get("featureType").get("title"))
None
>>> requests.put(
...     "http://localhost:9099/geoserver/rest/workspaces/test/datastores/test/featuretypes/situation.json",
...     json={
...         "featureType": {
...             "name": "situation",
...             "nativeName": "situation",
...             "internationalTitle": {"fr": "Titre de la couche", "en": "Layer title"},
...         }
...     },
...     auth=("admin", "geoserver"),
... )
<Response [200]>
>>> response = requests.get(
...     "http://localhost:9099/geoserver/rest/workspaces/test/datastores/test/featuretypes/situation.json",
...     auth=("admin", "geoserver"),
... )
>>> print(response.json().get("featureType").get("title"))
Titre de la couche
```
The title is `Titre de la couche` instead of `None`

Discussion:

Patch:
diff --git a/geoservercloud/geoservercloud.py b/geoservercloud/geoservercloud.py
index 7c1fea6..52a5b10 100644
--- a/geoservercloud/geoservercloud.py
+++ b/geoservercloud/geoservercloud.py
@@ -392,6 +392,16 @@ class GeoServerCloud:
         )
         return self.rest_service.create_feature_type(feature_type=feature_type)
 
+    def delete_feature_type(
+        self, workspace_name: str, datastore_name: str, layer_name: str
+    ) -> tuple[str, int]:
+        """
+        Delete a feature type and associated layer
+        """
+        return self.rest_service.delete_feature_type(
+            workspace_name, datastore_name, layer_name
+        )
+
     def create_layer_group(
         self,
         group: str,
diff --git a/geoservercloud/models/featuretype.py b/geoservercloud/models/featuretype.py
index d117d47..ae2d199 100644
--- a/geoservercloud/models/featuretype.py
+++ b/geoservercloud/models/featuretype.py
@@ -108,9 +108,9 @@ class FeatureType(EntityModel):
         feature_type = content["featureType"]
         workspace_name = feature_type["store"]["name"].split(":")[0]
         store_name = feature_type["store"]["name"].split(":")[1]
-        title = feature_type.get("title", feature_type.get("internationalTitle"))
+        title = feature_type.get("internationalTitle", feature_type.get("title"))
         abstract = feature_type.get(
-            "abstract", feature_type.get("internationalAbstract")
+            "internationalAbstract", feature_type.get("abstract")
         )
         if feature_type.get("metadataLinks"):
             metadata_links_payload = feature_type["metadataLinks"]["metadataLink"]
@@ -201,7 +201,13 @@ class FeatureType(EntityModel):
         return {"featureType": content}
 
     def put_payload(self) -> dict[str, Any]:
-        return self.post_payload()
+        content = self.post_payload()
+        # Force a null value on non-i18ned attributes, otherwise GeoServer sets it to the first i18n value
+        if content["featureType"].get("internationalTitle"):
+            content["featureType"]["title"] = None
+        if content["featureType"].get("internationalAbstract"):
+            content["featureType"]["abstract"] = None
+        return content
 
     def __repr__(self):
         return json.dumps(self.post_payload(), indent=4)
diff --git a/geoservercloud/services/restservice.py b/geoservercloud/services/restservice.py
index 9c5fd37..1ebdc87 100644
--- a/geoservercloud/services/restservice.py
+++ b/geoservercloud/services/restservice.py
@@ -288,6 +288,15 @@ class RestService:
             )
         return response.content.decode(), response.status_code
 
+    def delete_feature_type(
+        self, workspace_name: str, datastore_name: str, layer_name: str
+    ) -> tuple[str, int]:
+        response: Response = self.rest_client.delete(
+            self.rest_endpoints.featuretype(workspace_name, datastore_name, layer_name),
+            params={"recurse": "true"},
+        )
+        return response.content.decode(), response.status_code
+
     def create_layer_group(
         self,
         group: str,


Test patch:
diff --git a/tests/test_feature_type.py b/tests/test_feature_type.py
index a5e41b8..27346cd 100644
--- a/tests/test_feature_type.py
+++ b/tests/test_feature_type.py
@@ -147,6 +147,16 @@ def feature_type_post_payload(
     return {"featureType": content}
 
 
+@pytest.fixture(scope="module")
+def feature_type_put_payload(
+    feature_type_post_payload: dict[str, dict[str, Any]]
+) -> dict[str, dict[str, Any]]:
+    content = feature_type_post_payload.copy()
+    content["featureType"]["title"] = None
+    content["featureType"]["abstract"] = None
+    return content
+
+
 def test_get_feature_types(
     geoserver: GeoServerCloud, feature_types_get_response_payload: dict[str, Any]
 ) -> None:
@@ -218,7 +228,7 @@ def test_create_feature_type(
 
 
 def test_update_feature_type(
-    geoserver: GeoServerCloud, feature_type_post_payload: dict[str, dict[str, Any]]
+    geoserver: GeoServerCloud, feature_type_put_payload: dict[str, dict[str, Any]]
 ) -> None:
     with responses.RequestsMock() as rsps:
         rsps.get(
@@ -227,7 +237,7 @@ def test_update_feature_type(
         )
         rsps.put(
             f"{geoserver.url}/rest/workspaces/{WORKSPACE}/datastores/{STORE}/featuretypes/{LAYER}.json",
-            match=[responses.matchers.json_params_matcher(feature_type_post_payload)],
+            match=[responses.matchers.json_params_matcher(feature_type_put_payload)],
             status=200,
             body=b"",
         )
@@ -242,3 +252,19 @@ def test_update_feature_type(
 
         assert content == ""
         assert code == 200
+
+
+def test_delete_feature_type(geoserver: GeoServerCloud) -> None:
+    with responses.RequestsMock() as rsps:
+        rsps.delete(
+            f"{geoserver.url}/rest/workspaces/{WORKSPACE}/datastores/{STORE}/featuretypes/{LAYER}.json",
+            status=200,
+            body=b"",
+            match=[responses.matchers.query_param_matcher({"recurse": "true"})],
+        )
+        content, code = geoserver.delete_feature_type(
+            workspace_name=WORKSPACE, datastore_name=STORE, layer_name=LAYER
+        )
+
+        assert content == ""
+        assert code == 200