Repository: WeblateOrg/weblate

Issue:
Allow create, delete & update methods at the REST API languages endpoint
**Is your feature request related to a problem? Please describe.**

I would like to be able to create, delete and update languages via the API.

**Describe the solution you'd like**

`POST`, `DELETE`, `PATCH/PUT` methods available at the `/api/languages/` endpoint.

**Describe alternatives you've considered**

I have checked the REST API docs and there does not seem to be a valid way to do this.



Discussion:

Resolving patch:
diff --git a/weblate/api/serializers.py b/weblate/api/serializers.py
--- a/weblate/api/serializers.py
+++ b/weblate/api/serializers.py
@@ -22,6 +22,7 @@
 from rest_framework import serializers
 
 from weblate.auth.models import Group, Permission, Role, User
+from weblate.lang import data
 from weblate.lang.models import Language, Plural
 from weblate.screenshots.models import Screenshot
 from weblate.trans.defines import REPO_LENGTH
@@ -102,7 +103,7 @@ class Meta:
 
 class LanguageSerializer(serializers.ModelSerializer):
     web_url = AbsoluteURLField(source="get_absolute_url", read_only=True)
-    plural = LanguagePluralSerializer(read_only=True)
+    plural = LanguagePluralSerializer(required=False)
     aliases = serializers.ListField(source="get_aliases_names", read_only=True)
     statistics_url = serializers.HyperlinkedIdentityField(
         view_name="api:language-statistics", lookup_field="code"
@@ -127,19 +128,41 @@ class Meta:
 
     def validate_code(self, value):
         check_query = Language.objects.filter(code=value)
-        if check_query.exists() and not (
+        if not check_query.exists() and (
             isinstance(self.parent, ProjectSerializer)
             and self.field_name == "source_language"
         ):
-            raise serializers.ValidationError(
-                "Language with this Language code already exists."
-            )
-        if not check_query.exists():
             raise serializers.ValidationError(
                 "Language with this language code was not found."
             )
         return value
 
+    def validate_plural(self, value):
+        if not value and not (
+            isinstance(self.parent, ProjectSerializer)
+            and self.field_name == "source_language"
+        ):
+            raise serializers.ValidationError("This field is required.")
+        return value
+
+    def create(self, validated_data):
+        plural_validated = validated_data.pop("plural", None)
+        if not plural_validated:
+            raise serializers.ValidationError("No valid plural data was provided.")
+
+        check_query = Language.objects.filter(code=validated_data.get("code"))
+        if check_query.exists():
+            raise serializers.ValidationError(
+                "Language with this Language code already exists."
+            )
+        language = Language.objects.create(**validated_data)
+        plural = Plural(**plural_validated)
+        plural.language = language
+        plural.type = data.PLURAL_UNKNOWN
+        plural.source = Plural.SOURCE_DEFAULT
+        plural.save()
+        return language
+
 
 class UserSerializer(serializers.ModelSerializer):
     groups = serializers.HyperlinkedIdentityField(
diff --git a/weblate/api/views.py b/weblate/api/views.py
--- a/weblate/api/views.py
+++ b/weblate/api/views.py
@@ -935,7 +935,7 @@ def destroy(self, request, *args, **kwargs):
         return Response(status=status.HTTP_204_NO_CONTENT)
 
 
-class LanguageViewSet(viewsets.ReadOnlyModelViewSet):
+class LanguageViewSet(viewsets.ModelViewSet):
     """Languages API."""
 
     queryset = Language.objects.none()
@@ -943,8 +943,26 @@ class LanguageViewSet(viewsets.ReadOnlyModelViewSet):
     lookup_field = "code"
 
     def get_queryset(self):
+        if self.request.user.has_perm("language.edit"):
+            return Language.objects.order_by("id")
         return Language.objects.have_translation().order_by("id")
 
+    def perm_check(self, request):
+        if not request.user.has_perm("language.edit"):
+            self.permission_denied(request, message="Can not manage languages")
+
+    def update(self, request, *args, **kwargs):
+        self.perm_check(request)
+        return super().update(request, *args, **kwargs)
+
+    def create(self, request, *args, **kwargs):
+        self.perm_check(request)
+        return super().create(request, *args, **kwargs)
+
+    def destroy(self, request, *args, **kwargs):
+        self.perm_check(request)
+        return super().destroy(request, *args, **kwargs)
+
     @action(detail=True, methods=["get"])
     def statistics(self, request, **kwargs):
         obj = self.get_object()


Test patch:
diff --git a/weblate/api/tests.py b/weblate/api/tests.py
--- a/weblate/api/tests.py
+++ b/weblate/api/tests.py
@@ -1149,6 +1149,124 @@ def test_get_language(self):
         # Check for aliases
         self.assertEqual(len(response.data["aliases"]), 2)
 
+    def test_create(self):
+        self.do_request(
+            "api:language-list", method="post", code=403,
+        )
+        # Ensure it throws error without plural data
+        self.do_request(
+            "api:language-list",
+            method="post",
+            superuser=True,
+            code=400,
+            format="json",
+            request={"code": "new_lang", "name": "New Language", "direction": "rtl"},
+        )
+        response = self.do_request(
+            "api:language-list",
+            method="post",
+            superuser=True,
+            code=201,
+            format="json",
+            request={
+                "code": "new_lang",
+                "name": "New Language",
+                "direction": "rtl",
+                "plural": {"number": 2, "formula": "n != 1"},
+            },
+        )
+        self.assertEqual(Language.objects.count(), 366)
+        self.assertEqual(response.data["code"], "new_lang")
+        # Check that languages without translation are shown
+        # only to super users
+        response = self.do_request("api:language-list", method="get", code=200,)
+        self.assertEqual(response.data["count"], 4)
+        response = self.do_request(
+            "api:language-list", method="get", superuser=True, code=200,
+        )
+        self.assertEqual(response.data["count"], 366)
+        self.do_request(
+            "api:language-detail", kwargs={"code": "new_lang"}, method="get", code=404,
+        )
+        self.do_request(
+            "api:language-detail",
+            kwargs={"code": "new_lang"},
+            superuser=True,
+            method="get",
+            code=200,
+        )
+        # Creation with duplicate code gives 400
+        response = self.do_request(
+            "api:language-list",
+            method="post",
+            superuser=True,
+            code=400,
+            format="json",
+            request={
+                "code": "new_lang",
+                "name": "New Language",
+                "direction": "rtl",
+                "plural": {"number": 2, "formula": "n != 1"},
+            },
+        )
+
+    def test_delete(self):
+        self.do_request(
+            "api:language-list",
+            method="post",
+            superuser=True,
+            code=201,
+            format="json",
+            request={
+                "code": "new_lang",
+                "name": "New Language",
+                "direction": "rtl",
+                "plural": {"number": 2, "formula": "n != 1"},
+            },
+        )
+        self.do_request(
+            "api:language-detail",
+            kwargs={"code": "new_lang"},
+            method="delete",
+            superuser=True,
+            code=204,
+        )
+        self.assertEqual(Language.objects.count(), 365)
+
+    def test_put(self):
+        self.do_request(
+            "api:language-detail", kwargs={"code": "cs"}, method="put", code=403,
+        )
+        self.do_request(
+            "api:language-detail",
+            kwargs={"code": "cs"},
+            method="put",
+            superuser=True,
+            code=200,
+            format="json",
+            request={
+                "code": "cs",
+                "name": "New Language",
+                "direction": "rtl",
+                "plural": {"number": 2, "formula": "n != 1"},
+            },
+        )
+        self.assertEqual(Language.objects.get(code="cs").name, "New Language")
+
+    def test_patch(self):
+        self.do_request(
+            "api:language-detail", kwargs={"code": "cs"}, method="put", code=403,
+        )
+        self.do_request(
+            "api:language-detail",
+            kwargs={"code": "cs"},
+            method="patch",
+            superuser=True,
+            code=200,
+            request={"name": "New Language"},
+        )
+        self.assertEqual(Language.objects.get(code="cs").name, "New Language")
+
 
 class TranslationAPITest(APIBaseTest):
     def test_list_translations(self):