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

feat(api): use ValidationError with a field speficied when possible #13541

Merged
merged 1 commit into from
Jan 16, 2025
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
1 change: 1 addition & 0 deletions docs/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* :guilabel:`Synchronize` on shared repository now operates on all its components.
* :ref:`check-punctuation-spacing` ignores markup such as Markdown or reStructuredText.
* :ref:`autofix-punctuation-spacing` does not alter reStructuredText markup.
* Improved validation errors in :doc:`/api`.

**Bug fixes**

Expand Down Expand Up @@ -243,7 +244,7 @@
* :ref:`autofix` for French and Breton now uses a non-breaking space before colons instead of a narrow one.
* :ref:`api` now has a preview OpenAPI specification.
* Stale, empty glossaries are now automatically removed.
* :kbd:`?` now displays available :ref:`keyboard`.

Check warning on line 247 in docs/changes.rst

View workflow job for this annotation

GitHub Actions / Sphinx (de)

inconsistent term references in translated message. original: [':ref:`keyboard`'], translated: [] [i18n.inconsistent_references]
* Translation and language view in the project now include basic information about the language and plurals.
* :ref:`search-replace` shows a preview of matched strings.
* :ref:`aresource` now support translatable attribute in its strings.
Expand Down Expand Up @@ -365,7 +366,7 @@
* Weblate now uses mistletoe instead of misaka as a Markdown renderer.
* :ref:`csp` is now stricter what might block third-party customizations.
* Monolingual formats no longer copy comments from :ref:`component-template` when adding strings to translation.
* Dropped support for Amagama in :ref:`machine-translation-setup` as the service is no longer maintained.

Check warning on line 369 in docs/changes.rst

View workflow job for this annotation

GitHub Actions / Sphinx (ja)

inconsistent term references in translated message. original: [':ref:`machine-translation-setup`'], translated: [] [i18n.inconsistent_references]
* Default value for :setting:`SENTRY_SEND_PII` was changed.
* Translation credit reports in the JSON format now follows a different format for entries.

Expand Down Expand Up @@ -414,7 +415,7 @@

**Bug fixes**

* Language aliases in :doc:`/admin/machine`.

Check warning on line 418 in docs/changes.rst

View workflow job for this annotation

GitHub Actions / Sphinx (ja)

inconsistent term references in translated message. original: [':doc:`/admin/machine`'], translated: [] [i18n.inconsistent_references]

**Upgrading**

Expand Down Expand Up @@ -483,7 +484,7 @@

**Improvements**

* Visually highlight explanation in :ref:`glossary`.

Check warning on line 487 in docs/changes.rst

View workflow job for this annotation

GitHub Actions / Sphinx (ja)

inconsistent term references in translated message. original: [':ref:`glossary`'], translated: [] [i18n.inconsistent_references]
* Add :ref:`addons` history tab in management.
* New :ref:`alerts` when :ref:`glossary` might not work as expected.
* :doc:`/admin/announcements` can be posted on project/language scope.
Expand Down
60 changes: 27 additions & 33 deletions weblate/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
HTTP_200_OK,
HTTP_201_CREATED,
HTTP_204_NO_CONTENT,
HTTP_400_BAD_REQUEST,
HTTP_423_LOCKED,
HTTP_500_INTERNAL_SERVER_ERROR,
)
Expand Down Expand Up @@ -342,7 +341,7 @@
# Generate lookup
lookup = {}
category_path = ""
for field in reversed(self.lookup_fields):

Check failure on line 344 in weblate/api/views.py

View workflow job for this annotation

GitHub Actions / mypy

"MultipleFieldViewSet" has no attribute "lookup_fields"
if field not in {"component__slug", "slug"}:
lookup[field] = self.kwargs[field]
else:
Expand Down Expand Up @@ -429,12 +428,12 @@

if "group_id" not in request.data:
msg = "Missing group_id parameter"
raise ValidationError(msg)
raise ValidationError({"group_id": msg})

try:
group = Group.objects.get(pk=int(request.data["group_id"]))
except (Group.DoesNotExist, ValueError) as error:
raise ValidationError(str(error)) from error
raise ValidationError({"group_id": str(error)}) from error

Check warning

Code scanning / CodeQL

Information exposure through an exception Medium

Stack trace information
flows to this location and may be exposed to an external user.

if request.method == "POST":
obj.add_team(request, group)
Expand Down Expand Up @@ -542,7 +541,7 @@
return Response(serializer.data)


@extend_schema_view(

Check warning on line 544 in weblate/api/views.py

View workflow job for this annotation

GitHub Actions / API Lint

[GroupViewSet]: could not derive type of path parameter "language_code" because model "weblate.auth.models.Group" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".
retrieve=extend_schema(description="Return information about a group."),
partial_update=extend_schema(description="Change the group parameters."),
)
Expand All @@ -558,7 +557,7 @@
return Group.objects.order_by("id")
return self.request.user.groups.order_by(
"id"
) | self.request.user.administered_group_set.order_by("id")

Check failure on line 560 in weblate/api/views.py

View workflow job for this annotation

GitHub Actions / mypy

Item "AnonymousUser" of "User | AnonymousUser" has no attribute "administered_group_set"

Check failure on line 560 in weblate/api/views.py

View workflow job for this annotation

GitHub Actions / mypy

Unable to resolve return type of queryset/manager method "order_by"

def perm_check(self, request: Request, group: Group | None = None) -> None:
if (group is None and not self.request.user.has_perm("group.edit")) or (
Expand Down Expand Up @@ -589,12 +588,12 @@

if "role_id" not in request.data:
msg = "Missing role_id parameter"
raise ValidationError(msg)
raise ValidationError({"role_id": msg})

try:
role = Role.objects.get(pk=int(request.data["role_id"]))
except (Role.DoesNotExist, ValueError) as error:
raise ValidationError(str(error)) from error
raise ValidationError({"role_id": str(error)}) from error

Check warning

Code scanning / CodeQL

Information exposure through an exception Medium

Stack trace information
flows to this location and may be exposed to an external user.

obj.roles.add(role)
serializer = self.serializer_class(obj, context={"request": request})
Expand All @@ -612,12 +611,12 @@

if "language_code" not in request.data:
msg = "Missing language_code parameter"
raise ValidationError(msg)
raise ValidationError({"language_code": msg})

try:
language = Language.objects.get(code=request.data["language_code"])
except (Language.DoesNotExist, ValueError) as error:
raise ValidationError(str(error)) from error
raise ValidationError({"language_code": str(error)}) from error

Check warning

Code scanning / CodeQL

Information exposure through an exception Medium

Stack trace information
flows to this location and may be exposed to an external user.

obj.languages.add(language)
serializer = self.serializer_class(obj, context={"request": request})
Expand Down Expand Up @@ -650,14 +649,14 @@

if "project_id" not in request.data:
msg = "Missing project_id parameter"
raise ValidationError(msg)
raise ValidationError({"project_id": msg})

try:
project = Project.objects.get(
pk=int(request.data["project_id"]),
)
except (Project.DoesNotExist, ValueError) as error:
raise ValidationError(str(error)) from error
raise ValidationError({"project_id": str(error)}) from error

Check warning

Code scanning / CodeQL

Information exposure through an exception Medium

Stack trace information
flows to this location and may be exposed to an external user.
obj.projects.add(project)
serializer = self.serializer_class(obj, context={"request": request})

Expand Down Expand Up @@ -686,14 +685,14 @@

if "component_list_id" not in request.data:
msg = "Missing component_list_id parameter"
raise ValidationError(msg)
raise ValidationError({"component_list_id": msg})

try:
component_list = ComponentList.objects.get(
pk=int(request.data["component_list_id"]),
)
except (ComponentList.DoesNotExist, ValueError) as error:
raise ValidationError(str(error)) from error
raise ValidationError({"component_list_id": str(error)}) from error

Check warning

Code scanning / CodeQL

Information exposure through an exception Medium

Stack trace information
flows to this location and may be exposed to an external user.
obj.componentlists.add(component_list)
serializer = self.serializer_class(obj, context={"request": request})

Expand Down Expand Up @@ -732,14 +731,14 @@
self.perm_check(request)
if "component_id" not in request.data:
msg = "Missing component_id parameter"
raise ValidationError(msg)
raise ValidationError({"component_id": msg})

try:
component = Component.objects.filter_access(request.user).get(

Check failure on line 737 in weblate/api/views.py

View workflow job for this annotation

GitHub Actions / mypy

Argument 1 to "filter_access" of "ComponentQuerySet" has incompatible type "User | AnonymousUser"; expected "User"
pk=int(request.data["component_id"])
)
except (Component.DoesNotExist, ValueError) as error:
raise ValidationError(str(error)) from error
raise ValidationError({"component_id": str(error)}) from error

Check warning

Code scanning / CodeQL

Information exposure through an exception Medium

Stack trace information
flows to this location and may be exposed to an external user.
obj.components.add(component)
serializer = self.serializer_class(obj, context={"request": request})

Expand Down Expand Up @@ -768,13 +767,13 @@
user_id = request.data.get("user_id")
if not user_id:
msg = "User ID is required"
raise ValidationError(msg)
raise ValidationError({"user_id": msg})

try:
user = User.objects.get(pk=user_id)
except User.DoesNotExist as error:
msg = "User not found"
raise ValidationError(msg) from error
raise ValidationError({"user_id": msg}) from error
group.admins.add(user)
user.add_team(cast("AuthenticatedHttpRequest", request), group)
return Response({"Administration rights granted."}, status=HTTP_200_OK)
Expand Down Expand Up @@ -842,9 +841,9 @@
@action(detail=True, methods=["get"])
def credits(self, request, **kwargs):
if request.user.is_anonymous:
self.permission_denied(request, "Must be authenticated to get credits")

Check failure on line 844 in weblate/api/views.py

View workflow job for this annotation

GitHub Actions / mypy

"CreditsMixin" has no attribute "permission_denied"

obj = self.get_object()

Check failure on line 846 in weblate/api/views.py

View workflow job for this annotation

GitHub Actions / mypy

"CreditsMixin" has no attribute "get_object"

try:
start_date = from_current_timezone(
Expand All @@ -868,10 +867,10 @@
language = request.query_params["lang"]

data = generate_credits(
None if request.user.has_perm("reports.view", obj) else request.user,

Check failure on line 870 in weblate/api/views.py

View workflow job for this annotation

GitHub Actions / mypy

Argument 1 to "generate_credits" has incompatible type "Any | None"; expected "User"
start_date,
end_date,
language,

Check failure on line 873 in weblate/api/views.py

View workflow job for this annotation

GitHub Actions / mypy

Argument 4 to "generate_credits" has incompatible type "Any | None"; expected "str"
obj,
)
return Response(data=data)
Expand All @@ -888,7 +887,7 @@
):
"""Translation projects API."""

raw_urls: tuple[str, ...] = "project-file"

Check failure on line 890 in weblate/api/views.py

View workflow job for this annotation

GitHub Actions / mypy

Incompatible types in assignment (expression has type "str", variable has type "tuple[str, ...]")
raw_formats = ("zip", *(f"zip:{exporter}" for exporter in EXPORTERS))

queryset = Project.objects.none()
Expand All @@ -897,7 +896,7 @@
request: Request # type: ignore[assignment]

def get_queryset(self):
return self.request.user.allowed_projects.prefetch_related(

Check failure on line 899 in weblate/api/views.py

View workflow job for this annotation

GitHub Actions / mypy

Item "AnonymousUser" of "User | AnonymousUser" has no attribute "allowed_projects"
"addon_set"
).order_by("id")

Expand Down Expand Up @@ -1147,17 +1146,15 @@
if request.method in {"POST", "PATCH"}:
try:
service_name = request.data["service"]
except KeyError:
return Response(
{"errors": ["Missing service name"]}, status=HTTP_400_BAD_REQUEST
)
except KeyError as error:
raise ValidationError({"service": "Missing service name"}) from error

service, configuration, errors = validate_service_configuration(
service_name, request.data.get("configuration", "{}")
)

if service is None or errors:
return Response({"errors": errors}, status=HTTP_400_BAD_REQUEST)
raise ValidationError({"configuration": errors})

Check warning

Code scanning / CodeQL

Information exposure through an exception Medium

Stack trace information
flows to this location and may be exposed to an external user.

if request.method == "PATCH":
if configuration:
Expand All @@ -1178,10 +1175,7 @@

if request.method == "POST":
if service_name in project.machinery_settings:
return Response(
{"errors": ["Service already exists"]},
status=HTTP_400_BAD_REQUEST,
)
raise ValidationError({"service": ["Service already exists"]})

project.machinery_settings[service_name] = configuration
project.save(update_fields=["machinery_settings"])
Expand All @@ -1199,7 +1193,7 @@
)

if service is None or errors:
return Response({"errors": errors}, status=HTTP_400_BAD_REQUEST)
raise ValidationError({"configuration": errors})

Check warning

Code scanning / CodeQL

Information exposure through an exception Medium

Stack trace information
flows to this location and may be exposed to an external user.

valid_configurations[service_name] = configuration

Expand All @@ -1219,7 +1213,7 @@
)


@extend_schema_view(

Check warning on line 1216 in weblate/api/views.py

View workflow job for this annotation

GitHub Actions / API Lint

[ComponentViewSet]: could not derive type of path parameter "project_slug" because model "weblate.trans.models.component.Component" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".
list=extend_schema(description="Return a list of translation components."),
retrieve=extend_schema(
description="Return information about translation component."
Expand Down Expand Up @@ -1310,15 +1304,15 @@

if "language_code" not in request.data:
msg = "Missing 'language_code' parameter"
raise ValidationError(msg)
raise ValidationError({"languge_code": msg})

language_code = request.data["language_code"]

try:
language = Language.objects.get(code=language_code)
except Language.DoesNotExist as error:
msg = f"No language code {language_code!r} found!"
raise ValidationError(msg) from error
raise ValidationError({"language_code": msg}) from error

if not obj.can_add_new_language(request.user):
self.permission_denied(request, message=obj.new_lang_error_message)
Expand All @@ -1330,7 +1324,7 @@
message = "\n".join(m.message for m in storage)
else:
message = f"Could not add {language_code!r}!"
raise ValidationError(message)
raise ValidationError({"language_code": message})

serializer = TranslationSerializer(
translation, context={"request": request}, remove_fields=("component",)
Expand Down Expand Up @@ -1429,7 +1423,7 @@
self.permission_denied(request, "Can not edit component")
if "project_slug" not in request.data:
msg = "Missing 'project_slug' parameter"
raise ValidationError(msg)
raise ValidationError({"project_slug": msg})

project_slug = request.data["project_slug"]

Expand All @@ -1439,7 +1433,7 @@
)
except Project.DoesNotExist as error:
msg = f"No project slug {project_slug!r} found!"
raise ValidationError(msg) from error
raise ValidationError({"project_slug": msg}) from error

instance.links.add(project)
serializer = self.serializer_class(instance, context={"request": request})
Expand Down Expand Up @@ -1679,7 +1673,7 @@
parse_query(query_string)
except Exception as error:
msg = f"Could not parse query string: {error}"
raise ValidationError(msg) from error
raise ValidationError({"q": msg}) from error

Check warning

Code scanning / CodeQL

Information exposure through an exception Medium

Stack trace information
flows to this location and may be exposed to an external user.

queryset = obj.unit_set.search(query_string).order_by("id").prefetch_full()
page = self.paginate_queryset(queryset)
Expand Down Expand Up @@ -1821,7 +1815,7 @@
parse_query(query_string)
except Exception as error:
msg = f"Could not parse query string: {error}"
raise ValidationError(msg) from error
raise ValidationError({"q": msg}) from error

Check warning

Code scanning / CodeQL

Information exposure through an exception Medium

Stack trace information
flows to this location and may be exposed to an external user.
if query_string:
result = result.search(query_string)
return result
Expand Down Expand Up @@ -1855,7 +1849,7 @@
self.permission_denied(request, "The string is read-only.")
if not new_target or new_state is None:
msg = "Please provide both state and target for a partial update"
raise ValidationError(msg)
raise ValidationError({"state": msg, "target": msg})

if new_state not in {
STATE_APPROVED,
Expand Down Expand Up @@ -2091,7 +2085,7 @@
return Change.objects.preload_list(result)


@extend_schema_view(

Check warning on line 2088 in weblate/api/views.py

View workflow job for this annotation

GitHub Actions / API Lint

[ComponentListViewSet]: could not derive type of path parameter "component_slug" because model "weblate.trans.models.componentlist.ComponentList" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".
list=extend_schema(description="Return a list of component lists."),
retrieve=extend_schema(description="Return information about component list."),
partial_update=extend_schema(description="Change the component list parameters."),
Expand Down Expand Up @@ -2267,7 +2261,7 @@
return Response(serializer.data)


class Search(APIView):

Check failure on line 2264 in weblate/api/views.py

View workflow job for this annotation

GitHub Actions / API Lint

[Search]: unable to guess serializer. This is graceful fallback handling for APIViews. Consider using GenericAPIView as view base class, if view is under your control. Either way you may want to add a serializer_class (or method). Ignoring view for now.
"""Site-wide search endpoint."""

def get(self, request: Request, format=None): # noqa: A002
Expand Down Expand Up @@ -2323,7 +2317,7 @@
return Response(results)


@extend_schema_view(

Check failure on line 2320 in weblate/api/views.py

View workflow job for this annotation

GitHub Actions / API Lint

[TasksViewSet]: unable to guess serializer. This is graceful fallback handling for APIViews. Consider using GenericAPIView as view base class, if view is under your control. Either way you may want to add a serializer_class (or method). Ignoring view for now.
list=extend_schema(description="Listing of the tasks is currently not available.")
)
class TasksViewSet(ViewSet):
Expand Down
Loading