Skip to content
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
51 changes: 29 additions & 22 deletions weblate/auth/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,9 @@ def check_unit_review(

@register_perm("unit.edit", "suggestion.accept")
def check_edit_approved(
user: User, permission: str, obj: Unit | Translation | Component | Project
user: User,
permission: str,
obj: Unit | Translation | Component | Project | ProjectLanguage,
) -> bool | PermissionResult:
component = None
if isinstance(obj, Unit):
Expand Down Expand Up @@ -460,7 +462,7 @@ def check_suggestion_vote(

@register_perm("suggestion.add")
def check_suggestion_add(
user: User, permission: str, obj: Unit | Translation
user: User, permission: str, obj: Unit | Translation | ProjectLanguage
) -> bool | PermissionResult:
if isinstance(obj, Unit):
obj = obj.translation
Expand All @@ -469,6 +471,7 @@ def check_suggestion_add(
# Check contributor license agreement
if (
not user.is_bot
and isinstance(obj, Translation)
and obj.component.agreement
and not ContributorAgreement.objects.has_agreed(user, obj.component)
):
Expand All @@ -478,37 +481,41 @@ def check_suggestion_add(

@register_perm("upload.perform")
def check_upload(
user: User, permission: str, translation: Translation
user: User, permission: str, obj: Translation | ProjectLanguage
) -> bool | PermissionResult:
"""
Check whether user can perform any upload operation.

The actual check for the method is implemented in
weblate.trans.util.check_upload_method_permissions.
"""
# Source upload
if translation.is_source and not user.has_perm("source.edit", translation):
return Denied(gettext("Insufficient privileges for editing source strings."))
# Bilingual source translations
if (
translation.is_source
and not translation.is_template
and not issubclass(translation.component.file_format_cls, BilingualUpdateMixin)
):
return Denied(
gettext("The file format does not support updating source strings.")
)
if translation.component.is_glossary:
permission = "glossary.upload"
return check_can_edit(user, permission, translation) and (
if isinstance(obj, Translation):
# Source upload
if obj.is_source and not user.has_perm("source.edit", obj):
return Denied(
gettext("Insufficient privileges for editing source strings.")
)
# Bilingual source translations
if (
obj.is_source
and not obj.is_template
and not issubclass(obj.component.file_format_cls, BilingualUpdateMixin)
):
return Denied(
gettext("The file format does not support updating source strings.")
)
if obj.component.is_glossary:
permission = "glossary.upload"

return check_can_edit(user, permission, obj) and (
# Normal upload
check_edit_approved(user, "unit.edit", translation)
check_edit_approved(user, "unit.edit", obj)
# Suggestion upload
or check_suggestion_add(user, "suggestion.add", translation)
or check_suggestion_add(user, "suggestion.add", obj)
# Add upload
or check_suggestion_add(user, "unit.add", translation)
or check_suggestion_add(user, "unit.add", obj)
# Source upload
or translation.is_source
or (isinstance(obj, Translation) and obj.is_source)
)
Comment on lines +510 to 519
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The permission checks for check_upload now accept ProjectLanguage, but the function doesn't properly handle all the permission validation logic for ProjectLanguage objects. Specifically, when obj is a ProjectLanguage, the function proceeds directly to line 510 where it calls check_can_edit(user, permission, obj) and then checks various upload permissions.

However, the checks at lines 512-518 call functions like check_edit_approved, check_suggestion_add, etc., which expect either a Translation or a ProjectLanguage. While check_suggestion_add has been updated to support ProjectLanguage, these permission checks are designed to work on individual translations, not on project-language combinations. This could lead to confusing or incorrect permission results.

Consider adding explicit handling for ProjectLanguage in the check_upload function that either:

  1. Returns early with an appropriate message explaining that actual upload is not supported at this level (since this is meant to be an informational page)
  2. Implements proper permission logic that makes sense for a project-language context

Copilot uses AI. Check for mistakes.
Comment on lines 482 to 519
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes to support ProjectLanguage in permission checks (check_upload, check_suggestion_add, and check_edit_approved) lack test coverage. Given that comprehensive permission tests exist in weblate/auth/tests/test_permissions.py, new tests should be added to verify that the permission checks work correctly when called with a ProjectLanguage object.

Consider adding tests that verify:

  1. upload.perform permission works correctly with ProjectLanguage objects
  2. suggestion.add permission works correctly with ProjectLanguage objects
  3. unit.edit permission works correctly with ProjectLanguage objects
  4. The informational upload tab is displayed correctly when permissions are granted/denied

Copilot uses AI. Check for mistakes.


Expand Down
30 changes: 30 additions & 0 deletions weblate/templates/language-project.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

{% block nav_pills %}
{% perm 'project.edit' object.project as user_can_edit_project %}
{% perm 'upload.perform' object as user_can_upload_translation %}

<ul class="nav nav-pills">
<li class="nav-item">
Expand Down Expand Up @@ -63,6 +64,11 @@
href="{% url 'download' path=object.get_url_path %}?format=zip:xlsx"
title="{% translate "Download for offline translation." %}">{% blocktranslate %}Download translations as XLSX in a ZIP file{% endblocktranslate %}</a>
</li>
{% if user_can_upload_translation %}
<li>
<a class="dropdown-item" data-bs-target="#upload" data-bs-toggle="tab">{% translate "Upload translation" %}</a>
</li>
{% endif %}
Comment on lines +67 to +71
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new upload tab functionality for project-language pages lacks test coverage. Consider adding a test to verify that:

  1. The upload tab appears when a user has the appropriate permissions
  2. The upload tab is hidden when a user lacks permissions
  3. The informational message is displayed correctly
  4. The permission check doesn't cause runtime errors with ProjectLanguage objects

Copilot uses AI. Check for mistakes.
</ul>
</li>
<li class="nav-item dropdown">
Expand Down Expand Up @@ -127,6 +133,7 @@
{% announcements language=language project=project %}
{% perm 'project.edit' object.project as user_can_edit_project %}
{% get_translate_url object as translate_url %}
{% perm 'upload.perform' object as user_can_upload_translation %}

<div class="tab-content">

Expand Down Expand Up @@ -212,6 +219,29 @@ <h4 class="card-title">
</div>
{% endif %}

{% if user_can_upload_translation %}
<div class="tab-pane" id="upload">
<div class="card">
<div class="card-header">
<h4 class="card-title">
{% documentation_icon 'user/files' 'upload' right=True %}
{% translate "Upload" %}
</h4>
</div>
<div class="card-body">
<p>
{% blocktranslate trimmed %}
Project-wide uploads are currently not supported. Translation
files need to be uploaded on the individual translations. Switch
to the "Components" tab above, open the desired component, and
perform the upload there.
{% endblocktranslate %}
</p>
</div>
</div>
</div>
{% endif %}

{% if announcement_form %}
<div class="tab-pane" id="announcement">
<form action="{% url 'announcement' path=object.get_url_path %}" method="post">
Expand Down
4 changes: 4 additions & 0 deletions weblate/utils/stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -1106,6 +1106,10 @@ def __str__(self) -> str:
def enable_review(self) -> bool:
return self.project.enable_review

@property
def enable_suggestions(self) -> bool:
return True

Comment on lines +1111 to +1112
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The enable_suggestions property for ProjectLanguage is hardcoded to return True, which may not reflect the actual configuration of the components within the project-language combination. Some components within the project might have enable_suggestions set to False, or might have workflow settings that disable suggestions. This could lead to showing the upload tab to users even when suggestions are not actually enabled for any of the components in this project-language combination.

Consider checking if any translations within the project-language have suggestions enabled, or documenting why this always returns True is an acceptable approach for this use case.

Suggested change
return True
"""Return whether suggestions are enabled for this project-language.
Suggestions are considered enabled if any related component for this
project-language has suggestions enabled, or if workflow settings
explicitly enable them. This avoids advertising suggestions in the UI
when none of the components actually support them.
"""
# Check per-component configuration via related translations.
for translation in self.translation_set:
component = getattr(translation, "component", None)
if component is not None and getattr(component, "enable_suggestions", False):
return True
# Fallback to workflow settings, if they define suggestion behavior.
workflow = self.workflow_settings
if workflow is not None and hasattr(workflow, "enable_suggestions"):
return bool(getattr(workflow, "enable_suggestions"))
return False

Copilot uses AI. Check for mistakes.
@property
def is_readonly(self) -> bool:
return False
Expand Down
Loading