Skip to content

Commit

Permalink
models: add component categorization
Browse files Browse the repository at this point in the history
- add model for storing categories
- browsing of project categories
- support maximal depth of 3 categories rigth now

Fixes #263

TODO:

- add tests
    - UI
        - move category to another project
  • Loading branch information
nijel committed Aug 23, 2023
1 parent e692544 commit 491fa53
Show file tree
Hide file tree
Showing 44 changed files with 2,322 additions and 148 deletions.
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

0 comments on commit 491fa53

Please sign in to comment.