Skip to content

Commit

Permalink
API: Add update API for units
Browse files Browse the repository at this point in the history
* Added standalone serializers for update as we want to update only
  certain fields.
* The source/target attributes of unit are now arrays to properly handle
  plurals. Before this exposed internal plural representation.
* Added test to cover serializing and updating.

Fixes #4396
Issue #4394
  • Loading branch information
nijel committed Sep 18, 2020
1 parent 2c56345 commit b71af86
Show file tree
Hide file tree
Showing 5 changed files with 320 additions and 6 deletions.
36 changes: 34 additions & 2 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1835,20 +1835,26 @@ Units

.. http:get:: /api/units/(int:id)/
.. versionchanged:: 4.3

The ``target`` and ``source`` are now arrays to properly handle plural
strings.

Returns information about translation unit.

:param id: Unit ID
:type id: int
:>json string translation: URL of a related translation object
:>json string source: source string
:>json array source: source string
:>json string previous_source: previous source string used for fuzzy matching
:>json string target: target string
:>json array target: target string
:>json string id_hash: unique identifier of the unit
:>json string content_hash: unique identifier of the source string
:>json string location: location of the unit in source code
:>json string context: translation unit context
:>json string note: translation unit note
:>json string flags: translation unit flags
:>json int state: unit state, 0 - not translated, 10 - needs editing, 20 - translated, 30 - approved, 100 - read only
:>json boolean fuzzy: whether the unit is fuzzy or marked for review
:>json boolean translated: whether the unit is translated
:>json boolean approved: whether the translation is approved
Expand All @@ -1864,6 +1870,32 @@ Units
:>json string web_url: URL where the unit can be edited
:>json string souce_unit: Source unit link; see :http:get:`/api/units/(int:id)/`

.. http:patch:: /api/units/(int:id)/
.. versionadded:: 4.3

Performs partial update on translation unit.

:param id: Unit ID
:type id: int
:<json int state: unit state, 0 - not translated, 10 - needs editing, 20 - translated, 30 - approved, 100 - read only
:<json array target: target string
:<json string explanation: String explanation, available on source units, see :ref:`additional`
:<json string extra_flags: Additiona string flags, available on source units, see :ref:`custom-checks`

.. http:put:: /api/units/(int:id)/
.. versionadded:: 4.3

Performs full update on translation unit.

:param id: Unit ID
:type id: int
:<json int state: unit state, 0 - not translated, 10 - needs editing, 20 - translated, 30 - approved, 100 - read only
:<json array target: target string
:<json string explanation: String explanation, available on source units, see :ref:`additional`
:<json string extra_flags: Additiona string flags, available on source units, see :ref:`custom-checks`

Changes
+++++++

Expand Down
3 changes: 2 additions & 1 deletion docs/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ Not yet released.
* Improved look of matrix mode.
* Machinery is now called automatic suggestions.
* Added support for interacting with multiple GitLab or GitHub instances.
* Extended API to cover project updates.
* Extended API to cover project updates, unit updates.
* Unit API now properly handles plural strings.
* Support markdown in contributor agreement.
* Improved source strings tracking.

Expand Down
47 changes: 47 additions & 0 deletions weblate/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,11 @@ def to_representation(self, instance):
return result


class PluralField(serializers.ListField):
def get_attribute(self, instance):
return getattr(instance, f"get_{self.field_name}_plurals")()


class UnitSerializer(serializers.ModelSerializer):
web_url = AbsoluteURLField(source="get_absolute_url", read_only=True)
translation = MultiFieldHyperlinkedIdentityField(
Expand All @@ -730,6 +735,8 @@ class UnitSerializer(serializers.ModelSerializer):
source_unit = serializers.HyperlinkedRelatedField(
read_only=True, view_name="api:unit-detail"
)
source = PluralField()
target = PluralField()

class Meta:
model = Unit
Expand All @@ -744,6 +751,7 @@ class Meta:
"context",
"note",
"flags",
"state",
"fuzzy",
"translated",
"approved",
Expand All @@ -763,6 +771,45 @@ class Meta:
extra_kwargs = {"url": {"view_name": "api:unit-detail"}}


class ReadonlySourceUnitWriteSerializer(serializers.ModelSerializer):
"""Serializer for updating readonly source unit."""

class Meta:
model = Unit
fields = (
"explanation",
"extra_flags",
)


class SourceUnitWriteSerializer(serializers.ModelSerializer):
"""Serializer for updating source unit."""

target = PluralField()

class Meta:
model = Unit
fields = (
"target",
"state",
"explanation",
"extra_flags",
)


class UnitWriteSerializer(serializers.ModelSerializer):
"""Serializer for updating target unit."""

target = PluralField()

class Meta:
model = Unit
fields = (
"target",
"state",
)


class ScreenshotSerializer(RemovableSerializer):
component = MultiFieldHyperlinkedIdentityField(
view_name="api:component-detail",
Expand Down
157 changes: 156 additions & 1 deletion weblate/api/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -1887,9 +1887,164 @@ def test_list_units(self):
self.assertEqual(response.data["count"], 16)

def test_get_unit(self):
unit = Unit.objects.filter(translation__language_code="cs")[0]
unit = Unit.objects.get(
translation__language_code="cs", source="Hello, world!\n"
)
response = self.client.get(reverse("api:unit-detail", kwargs={"pk": unit.pk}))
self.assertIn("translation", response.data)
self.assertEqual(response.data["source"], ["Hello, world!\n"])

def test_get_plural_unit(self):
unit = Unit.objects.get(
translation__language_code="cs", source__startswith="Orangutan has "
)
response = self.client.get(reverse("api:unit-detail", kwargs={"pk": unit.pk}))
self.assertIn("translation", response.data)
self.assertEqual(
response.data["source"],
["Orangutan has %d banana.\n", "Orangutan has %d bananas.\n"],
)

def test_translate_unit(self):
unit = Unit.objects.get(
translation__language_code="cs", source="Hello, world!\n"
)
# Changing state only
self.do_request(
"api:unit-detail",
kwargs={"pk": unit.pk},
method="patch",
code=400,
request={"state": "20"},
)
# Changing target only
self.do_request(
"api:unit-detail",
kwargs={"pk": unit.pk},
method="patch",
code=400,
request={"target": "Test translation"},
)
# Performing update
self.do_request(
"api:unit-detail",
kwargs={"pk": unit.pk},
method="patch",
code=200,
request={"state": "20", "target": "Test translation"},
)
# Invalid state changes
self.do_request(
"api:unit-detail",
kwargs={"pk": unit.pk},
method="patch",
code=400,
request={"state": "100", "target": "Test read only translation"},
)
self.do_request(
"api:unit-detail",
kwargs={"pk": unit.pk},
method="patch",
code=400,
request={"state": "0", "target": "Test read only translation"},
)
self.do_request(
"api:unit-detail",
kwargs={"pk": unit.pk},
method="patch",
code=400,
request={"state": "20", "target": ""},
)
unit = Unit.objects.get(pk=unit.pk)
# The auto fixer adds the trailing newline
self.assertEqual(unit.target, "Test translation\n")

def test_unit_review(self):
self.component.project.translation_review = True
self.component.project.save()
unit = Unit.objects.get(
translation__language_code="cs", source="Hello, world!\n"
)
# Changing to approved is not allowed without perms
self.do_request(
"api:unit-detail",
kwargs={"pk": unit.pk},
method="patch",
code=403,
request={"state": "30", "target": "Test translation"},
)
self.assertFalse(Unit.objects.get(pk=unit.pk).approved)

# Changing state to approved
self.do_request(
"api:unit-detail",
kwargs={"pk": unit.pk},
method="patch",
code=200,
superuser=True,
request={"state": "30", "target": "Test translation"},
)
self.assertTrue(Unit.objects.get(pk=unit.pk).approved)

# Changing approved unit is not allowed
self.do_request(
"api:unit-detail",
kwargs={"pk": unit.pk},
method="patch",
code=403,
request={"state": "20", "target": "Test translation"},
)
self.assertTrue(Unit.objects.get(pk=unit.pk).approved)

def test_translate_source_unit(self):
unit = Unit.objects.get(
translation__language_code="en", source="Hello, world!\n"
)
# The params are silently ignored here
self.do_request(
"api:unit-detail",
kwargs={"pk": unit.pk},
method="patch",
code=200,
request={"state": "20", "target": "Test translation"},
)
unit = Unit.objects.get(pk=unit.pk)
self.assertEqual(unit.target, "Hello, world!\n")
# Actual update
self.do_request(
"api:unit-detail",
kwargs={"pk": unit.pk},
method="patch",
code=200,
superuser=True,
request={"explanation": "This is good explanation"},
)
# No permissions
self.do_request(
"api:unit-detail",
kwargs={"pk": unit.pk},
method="patch",
code=403,
request={"explanation": "This is wrong explanation"},
)
unit = Unit.objects.get(pk=unit.pk)
self.assertEqual(unit.explanation, "This is good explanation")

def test_translate_plural_unit(self):
unit = Unit.objects.get(
translation__language_code="cs", source__startswith="Orangutan has "
)
self.do_request(
"api:unit-detail",
kwargs={"pk": unit.pk},
method="patch",
code=200,
format="json",
request={"state": 20, "target": ["singular", "many", "other"]},
)
unit = Unit.objects.get(pk=unit.pk)
# The auto fixer adds the trailing newline
self.assertEqual(unit.get_target_plurals(), ["singular\n", "many\n", "other\n"])


class ScreenshotAPITest(APIBaseTest):
Expand Down

0 comments on commit b71af86

Please sign in to comment.