Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add component categorization #9731

Merged
merged 1 commit into from
Aug 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion docs/admin/projects.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@ Translation projects
Translation organization
------------------------

Weblate organizes translatable VCS content of project/components into a tree-like structure.
Weblate organizes translatable VCS content of project/components into a
tree-like structure. You can additionally organize components within a project
using categories.

* The bottom level object is :ref:`project`, which should hold all translations belonging
together (for example translation of an application in several versions
and/or accompanying documentation).

* The middle level is optionally created by :ref:`category`. The categories can
be nested to achieve more complex structure.

* On the level above, :ref:`component`, which is
actually the component to translate, you define the VCS repository to use, and
the mask of files to translate.
Expand Down Expand Up @@ -947,6 +952,14 @@ Glossary color

Display color for a glossary used when showing word matches.

.. _category:

Category
--------

Categories are there to give structure to components within a project. You can
nest them to achieve a more complex structure.

.. _markup:

Template markup
Expand Down
75 changes: 75 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,17 @@ form submission (:mimetype:`application/x-www-form-urlencoded`) or as JSON
-H "Authorization: Token TOKEN" \
http://example.com/api/components/hello/weblate/repository/

.. _api-category:

Components and categories
~~~~~~~~~~~~~~~~~~~~~~~~~

To access a component which is nested inside a :ref:`category`, you need to URL
encode the category name into a component name separated with a slash. For
example ``usage`` placed in a ``docs`` category needs to be used as
``docs%2Fusage``. Full URL in this case would be for example
``https://example.com/api/components/hello/docs%2Fusage/repository/``.

.. _api-rate:

API rate limiting
Expand Down Expand Up @@ -1125,6 +1136,15 @@ Projects

Returned attributes are described in :ref:`api-statistics`.

.. http:get:: /api/projects/(string:project)/categories/

.. versionadded:: 5.0

Returns statistics for a project. See :http:get:`/api/categories/(int:id)/` for field definitions.

:param project: Project URL slug
:type project: string

Components
++++++++++

Expand Down Expand Up @@ -2472,6 +2492,61 @@ Search
:>json str url: Web URL of the matched item.
:>json str category: Category of the matched item.

Categories
++++++++++

.. http:get:: /api/categories/

.. versionadded:: 5.0

Lists available categories. See :http:get:`/api/categories/(int:id)/` for field definitions.

.. http:post:: /api/categories/

.. versionadded:: 5.0

Creates a new category. See :http:get:`/api/categories/(int:id)/` for field definitions.

.. http:get:: /api/categories/(int:id)/

.. versionadded:: 5.0

:param id: Category ID
:type id: int
:>json str name: Name of category.
:>json str slug: Slug of category.
:>json str project: Link to a project.
:>json str category: Link to a parent category.

.. http:patch:: /api/categories/(int:id)/

.. versionadded:: 5.0

Edit partial information about cateogry.

:param id: Category ID
:type id: int
:>json object configuration: Optional cateogry configuration

.. http:put:: /api/categories/(int:id)/

.. versionadded:: 5.0

Edit full information about cateogry.

:param id: Category ID
:type id: int
:>json object configuration: Optional cateogry configuration

.. http:delete:: /api/categories/(int:id)/

.. versionadded:: 5.0

Delete cateogry.

:param id: Category ID
:type id: int

.. _hooks:

Notification hooks
Expand Down
1 change: 1 addition & 0 deletions docs/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Not yet released.
**New features**

* :doc:`/formats/markdown` support, thanks to Anders Kaplan.
* :ref:`category` can now organize components within a project.
* :doc:`/formats/fluent` now has better syntax checks thanks to Henry Wilkes.
* Inviting users now works with all authentication methods.
* Docker container supports file backed secrets, see :ref:`docker-secrets`.
Expand Down
76 changes: 74 additions & 2 deletions weblate/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from weblate.trans.defines import BRANCH_LENGTH, LANGUAGE_NAME_LENGTH, REPO_LENGTH
from weblate.trans.models import (
AutoComponentList,
Category,
Change,
Component,
ComponentList,
Expand Down Expand Up @@ -55,13 +56,21 @@ def get_url(self, obj, view_name, request, format):
return None

kwargs = {}
was_slug = False
for lookup in self.lookup_field:
value = obj
for key in lookup.split("__"):
# NULL value
if value is None:
return None
previous = value
value = getattr(value, key)
if key == "slug":
if was_slug and previous.category:
value = "%2F".join(
(*previous.category.get_url_path()[1:], value)
)
was_slug = True
if self.strip_parts:
lookup = "__".join(lookup.split("__")[self.strip_parts :])
kwargs[lookup] = value
Expand Down Expand Up @@ -348,6 +357,9 @@ class ProjectSerializer(serializers.ModelSerializer):
statistics_url = serializers.HyperlinkedIdentityField(
view_name="api:project-statistics", lookup_field="slug"
)
categories_url = serializers.HyperlinkedIdentityField(
view_name="api:project-categories", lookup_field="slug"
)
languages_url = serializers.HyperlinkedIdentityField(
view_name="api:project-languages", lookup_field="slug"
)
Expand All @@ -364,6 +376,7 @@ class Meta:
"components_list_url",
"repository_url",
"statistics_url",
"categories_url",
"changes_list_url",
"languages_url",
"translation_review",
Expand Down Expand Up @@ -452,6 +465,13 @@ class ComponentSerializer(RemovableSerializer):

enforced_checks = serializers.JSONField(required=False)

category = serializers.HyperlinkedRelatedField(
view_name="api:category-detail",
queryset=Category.objects.none(),
required=False,
allow_null=True,
)

task_url = RelatedTaskField(lookup_field="background_task_id")

addons = serializers.HyperlinkedIdentityField(
Expand Down Expand Up @@ -525,6 +545,7 @@ class Meta:
"is_glossary",
"glossary_color",
"disable_autoshare",
"category",
)
extra_kwargs = {
"url": {
Expand All @@ -533,6 +554,11 @@ class Meta:
}
}

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if isinstance(self.instance, Component):
self.fields["category"].queryset = self.instance.project.category_set.all()

def validate_enforced_checks(self, value):
if not isinstance(value, list):
raise serializers.ValidationError("Enforced checks has to be a list.")
Expand Down Expand Up @@ -602,9 +628,14 @@ def validate(self, attrs):
if self.instance and getattr(self.instance, field) == attrs[field]:
continue
# Look for existing components
if attrs["project"].component_set.filter(**{field: attrs[field]}).exists():
project = attrs["project"]
field_filter = {field: attrs[field]}
if (
project.component_set.filter(**field_filter).exists()
or project.category_set.filter(**field_filter).exists()
):
raise serializers.ValidationError(
{field: f"Component with this {field} already exists."}
{field: f"Component or category with this {field} already exists."}
)

# Handle uploaded files
Expand Down Expand Up @@ -1086,6 +1117,47 @@ def as_kwargs(self, data=None):
}


class CategorySerializer(RemovableSerializer):
project = serializers.HyperlinkedRelatedField(
view_name="api:project-detail",
lookup_field="slug",
queryset=Project.objects.none(),
required=True,
)
category = serializers.HyperlinkedRelatedField(
view_name="api:category-detail",
queryset=Category.objects.none(),
required=False,
)

class Meta:
model = Category
fields = (
"name",
"slug",
"project",
"category",
"url",
)
extra_kwargs = {"url": {"view_name": "api:category-detail"}}

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
user = self.context["request"].user
self.fields["project"].queryset = user.managed_projects

def validate(self, attrs):
# Call model validation here, DRF does not do that
if self.instance:
instance = copy(self.instance)
for key, value in attrs.items():
setattr(instance, key, value)
else:
instance = Category(**attrs)
instance.clean()
return attrs


class ScreenshotSerializer(RemovableSerializer):
translation = MultiFieldHyperlinkedIdentityField(
view_name="api:translation-detail",
Expand Down
Loading