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

10896 Duplicate component shortcut #12320

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 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 @@ -12,6 +12,7 @@ Not yet released.
* :ref:`vcs-bitbucket-cloud`.

**Improvements**
* A shortcut to duplicate a component is now available directly in the menu (:guilabel:`Manage` → :guilabel:`Duplicate Component`)

* :ref:`mt-modernmt` supports :ref:`glossary-mt`.
* :ref:`mt-deepl` now supports specifying translation context.
Expand Down
1 change: 1 addition & 0 deletions weblate/templates/component.html
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
<li><a href="{% url 'manage-access' project=object.project.slug %}">{% trans "Users" %}</a></li>
{% endif %}
{% if user_can_edit_component %}
<li><a href="{% url 'create-component' %}?component={{ object.id }}#existing">{% trans "Duplicate Component" %}</li>
<li><a href="{% url 'guide' path=object.get_url_path %}">{% trans "Community localization checklist" %}</a></li>
<li><a href="{% url 'addons' path=object.get_url_path %}">{% trans "Add-ons" %}</a></li>
{% endif %}
Expand Down
6 changes: 6 additions & 0 deletions weblate/trans/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -1657,6 +1657,12 @@ def clean(self) -> None:
class ComponentCreateForm(SettingsBaseForm, ComponentDocsMixin, ComponentAntispamMixin):
"""Component creation form."""

source_component = forms.ModelChoiceField(
queryset=Component.objects.none(),
required=False,
widget=forms.HiddenInput(),
)

class Meta:
model = Component
fields = [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Copyright © Michal Čihař <[email protected]>
#
# SPDX-License-Identifier: GPL-3.0-or-later

# Generated by Django 5.0.6 on 2024-09-30 10:38

from django.db import migrations, models

import weblate.utils.render


class Migration(migrations.Migration):
dependencies = [
("trans", "0024_component_key_filter"),
]

operations = [
migrations.AlterField(
model_name="component",
name="add_message",
field=models.TextField(
blank=True,
default="Added translation using Weblate ({{ language_name }})\n\n",
help_text="You can use template language for various info, please consult the documentation for more details.",
validators=[weblate.utils.render.validate_render_commit],
verbose_name="Commit message when adding translation",
),
),
migrations.AlterField(
model_name="component",
name="addon_message",
field=models.TextField(
blank=True,
default='Update translation files\n\nUpdated by "{{ addon_name }}" add-on in Weblate.\n\nTranslation: {{ project_name }}/{{ component_name }}\nTranslate-URL: {{ url }}',
help_text="You can use template language for various info, please consult the documentation for more details.",
validators=[weblate.utils.render.validate_render_addon],
verbose_name="Commit message when add-on makes a change",
),
),
migrations.AlterField(
model_name="component",
name="delete_message",
field=models.TextField(
blank=True,
default="Deleted translation using Weblate ({{ language_name }})\n\n",
help_text="You can use template language for various info, please consult the documentation for more details.",
validators=[weblate.utils.render.validate_render_commit],
verbose_name="Commit message when removing translation",
),
),
migrations.AlterField(
model_name="component",
name="merge_message",
field=models.TextField(
blank=True,
default="Merge branch '{{ component_remote_branch }}' into Weblate.\n\n",
help_text="You can use template language for various info, please consult the documentation for more details.",
validators=[weblate.utils.render.validate_render_component],
verbose_name="Commit message when merging translation",
),
),
migrations.AlterField(
model_name="component",
name="merge_style",
field=models.CharField(
blank=True,
choices=[
("merge", "Merge"),
("rebase", "Rebase"),
("merge_noff", "Merge without fast-forward"),
],
default="rebase",
help_text="Define whether Weblate should merge the upstream repository or rebase changes onto it.",
max_length=10,
verbose_name="Merge style",
),
),
migrations.AlterField(
model_name="component",
name="pull_message",
field=models.TextField(
blank=True,
default="Translations update from {{ site_title }}\n\nTranslations update from [{{ site_title }}]({{ site_url }}) for [{{ project_name }}/{{ component_name }}]({{url}}).\n\n{% if component_linked_childs %}\nIt also includes following components:\n{% for linked in component_linked_childs %}\n* [{{ linked.project_name }}/{{ linked.name }}]({{ linked.url }})\n{% endfor %}\n{% endif %}\n\nCurrent translation status:\n\n![Weblate translation status]({{widget_url}})\n",
help_text="You can use template language for various info, please consult the documentation for more details.",
validators=[weblate.utils.render.validate_render_addon],
verbose_name="Merge request message",
),
),
]
6 changes: 6 additions & 0 deletions weblate/trans/models/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,7 @@
verbose_name=gettext_lazy("Merge style"),
max_length=10,
choices=MERGE_CHOICES,
blank=True,
default=settings.DEFAULT_MERGE_STYLE,
help_text=gettext_lazy(
"Define whether Weblate should merge the upstream repository "
Expand All @@ -636,6 +637,7 @@
"please consult the documentation for more details."
),
validators=[validate_render_commit],
blank=True,
default=settings.DEFAULT_ADD_MESSAGE,
)
delete_message = models.TextField(
Expand All @@ -645,6 +647,7 @@
"please consult the documentation for more details."
),
validators=[validate_render_commit],
blank=True,
default=settings.DEFAULT_DELETE_MESSAGE,
)
merge_message = models.TextField(
Expand All @@ -654,6 +657,7 @@
"please consult the documentation for more details."
),
validators=[validate_render_component],
blank=True,
default=settings.DEFAULT_MERGE_MESSAGE,
)
addon_message = models.TextField(
Expand All @@ -663,6 +667,7 @@
"please consult the documentation for more details."
),
validators=[validate_render_addon],
blank=True,
default=settings.DEFAULT_ADDON_MESSAGE,
)
pull_message = models.TextField(
Expand All @@ -672,6 +677,7 @@
"please consult the documentation for more details."
),
validators=[validate_render_addon],
blank=True,
gersona marked this conversation as resolved.
Show resolved Hide resolved
default=settings.DEFAULT_PULL_MESSAGE,
)
push_on_commit = models.BooleanField(
Expand Down Expand Up @@ -1104,7 +1110,7 @@
# Calculate progress for translations
if progress is None:
self.translations_progress += 1
progress = 100 * self.translations_progress // self.translations_count

Check failure on line 1113 in weblate/trans/models/component.py

View workflow job for this annotation

GitHub Actions / mypy

Unsupported operand types for // ("int" and "None")
# Store task state
current_task.update_state(
state="PROGRESS", meta={"progress": progress, "component": self.pk}
Expand Down Expand Up @@ -1753,7 +1759,7 @@
from weblate.trans.tasks import perform_push

self.log_info("scheduling push")
perform_push.delay_on_commit(

Check failure on line 1762 in weblate/trans/models/component.py

View workflow job for this annotation

GitHub Actions / mypy

"Task[[Any, VarArg(Any), KwArg(Any)], None]" has no attribute "delay_on_commit"
self.pk, None, force_commit=False, do_update=do_update
)

Expand Down Expand Up @@ -2340,7 +2346,7 @@
.order_by("-id")[0]
.auto_status
):
self.do_lock(user=None, lock=False, auto=True)

Check failure on line 2349 in weblate/trans/models/component.py

View workflow job for this annotation

GitHub Actions / mypy

Argument "user" to "do_lock" of "Component" has incompatible type "None"; expected "User"

if ALERTS[alert].link_wide:
for component in self.linked_childs:
Expand All @@ -2358,7 +2364,7 @@

# Automatically lock on error
if created and self.auto_lock_error and alert in LOCKING_ALERTS:
self.do_lock(user=None, lock=True, auto=True)

Check failure on line 2367 in weblate/trans/models/component.py

View workflow job for this annotation

GitHub Actions / mypy

Argument "user" to "do_lock" of "Component" has incompatible type "None"; expected "User"

# Update details with exception of component removal
if not created and not noupdate:
Expand Down Expand Up @@ -2563,7 +2569,7 @@
)
self.handle_parse_error(error.__cause__, filename=self.template)
self.update_import_alerts()
raise error.__cause__ from error # pylint: disable=E0710

Check failure on line 2572 in weblate/trans/models/component.py

View workflow job for this annotation

GitHub Actions / mypy

Exception must be derived from BaseException
was_change |= bool(translation.reason)
translations[translation.id] = translation
languages[lang.code] = translation
Expand Down Expand Up @@ -2627,7 +2633,7 @@
if self.needs_cleanup and not self.template:
from weblate.trans.tasks import cleanup_component

cleanup_component.delay_on_commit(self.id)

Check failure on line 2636 in weblate/trans/models/component.py

View workflow job for this annotation

GitHub Actions / mypy

"Task[[Any], None]" has no attribute "delay_on_commit"

if was_change:
if self.needs_variants_update:
Expand Down Expand Up @@ -2659,7 +2665,7 @@
if settings.CELERY_TASK_ALWAYS_EAGER:
batch_update_checks(self.id, batched_checks, component=self)
else:
batch_update_checks.delay_on_commit(self.id, batched_checks)

Check failure on line 2668 in weblate/trans/models/component.py

View workflow job for this annotation

GitHub Actions / mypy

"Task[[Any, Any, Component | None], None]" has no attribute "delay_on_commit"
self.batch_checks = False
self.batched_checks = set()

Expand Down Expand Up @@ -2810,7 +2816,7 @@
raise ValidationError(
{"filemask": gettext("The file mask did not match any files.")}
)
langs = {}

Check failure on line 2819 in weblate/trans/models/component.py

View workflow job for this annotation

GitHub Actions / mypy

Need type annotation for "langs" (hint: "langs: dict[<type>, <type>] = ...")
existing_langs = set()

for match in matches:
Expand Down Expand Up @@ -2878,7 +2884,7 @@
if (not self.new_base and self.new_lang != "add") or not self.file_format:
return
# File is valid or no file is needed
errors = []

Check failure on line 2887 in weblate/trans/models/component.py

View workflow job for this annotation

GitHub Actions / mypy

Need type annotation for "errors" (hint: "errors: list[<type>] = ...")
if self.is_valid_base_for_new(errors):
return
# File is needed, but not present
Expand Down Expand Up @@ -3249,7 +3255,7 @@
if self.variant_regex:
variant_re = re.compile(self.variant_regex)
units = process_units.filter(context__regex=self.variant_regex)
variant_updates = {}

Check failure on line 3258 in weblate/trans/models/component.py

View workflow job for this annotation

GitHub Actions / mypy

Need type annotation for "variant_updates" (hint: "variant_updates: dict[<type>, <type>] = ...")
for unit in units.iterator():
if variant_re.findall(unit.context):
key = variant_re.sub("", unit.context)
Expand Down
104 changes: 102 additions & 2 deletions weblate/trans/tests/test_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

"""Test for creating projects and models."""

import urllib.parse

from django.core.files.uploadedfile import SimpleUploadedFile
from django.test.utils import modify_settings, override_settings
from django.urls import reverse
Expand Down Expand Up @@ -179,22 +181,120 @@ def test_create_component_wizard(self) -> None:

@modify_settings(INSTALLED_APPS={"remove": "weblate.billing"})
def test_create_component_existing(self) -> None:
from weblate.trans.models import Component

# Make superuser
self.user.is_superuser = True
self.user.save()

self.component.agreement = "test agreement"
self.component.merge_style = "merge"
self.component.commit_message = "test commit_message"
self.component.add_message = "test add_message"
self.component.delete_message = "test delete_message"
self.component.merge_message = "test merge_message"
self.component.addon_message = "test addon_message"
self.component.pull_message = "test pull_message"
self.component.save()

response = self.client.get(
reverse("create-component") + f"?component={self.component.pk}#existing",
follow=True,
)
# init step
self.assertContains(response, "Create component")

with override_settings(CREATE_GLOSSARIES=self.CREATE_GLOSSARIES):
response = self.client.post(
reverse("create-component"),
{
"origin": "existing",
"name": "Create Component",
"slug": "create-component",
"name": "Create Component From Existing",
"slug": "create-component-from-existing",
"component": self.component.pk,
"is_glossary": self.component.is_glossary,
},
follow=True,
)

self.assertContains(response, self.component.get_repo_link_url())
parsed_query = urllib.parse.parse_qs(response.request["QUERY_STRING"])
expected_query_strings = [
"vcs",
"source_language",
"license",
]
for field in expected_query_strings:
if component_value := getattr(self.component, field):
if field == "source_language":
component_value = str(component_value.id)
self.assertEqual(parsed_query[field][0], component_value)

self.assertEqual(parsed_query["source_component"][0], str(self.component.pk))

# discovery step
self.assertContains(response, "Choose translation files to import")

with override_settings(CREATE_GLOSSARIES=self.CREATE_GLOSSARIES):
response = self.client.post(
reverse("create-component-vcs")
+ f"?source_component={self.component.pk}#existing,",
{
"name": "Create Component From Existing",
"slug": "create-component-from-existing",
"is_glossary": self.component.is_glossary,
"project": self.component.project_id,
"vcs": self.component.vcs,
"repo": self.component.repo,
"discovery": 28, # deep/*/locales/*/LC_MESSAGES/messages.po
"source_language": self.component.source_language_id,
},
follow=True,
)
self.assertContains(
response,
"You will be able to edit more options in the component settings after creating it.",
)

with override_settings(CREATE_GLOSSARIES=self.CREATE_GLOSSARIES):
response = self.client.post(
reverse("create-component-vcs")
+ f"?source_component={self.component.pk}#existing,",
{
"name": "Create Component From Existing",
"slug": "create-component-from-existing",
"is_glossary": self.component.is_glossary,
"project": self.component.project_id,
"vcs": self.component.vcs,
"repo": self.component.repo,
"source_language": self.component.source_language_id,
"file_format": "po",
"filemask": "deep/*/locales/*/LC_MESSAGES/messages.po",
"new_lang": "add",
"new_base": "deep/cs/locales/cs/LC_MESSAGES/messages.po",
"language_regex": "^[^.]+$",
"source_component": self.component.pk,
},
follow=True,
)
self.assertContains(response, "Community localization checklist")
self.assertContains(response, "Test/Create Component From Existing @ Weblate")

new_component = Component.objects.get(name="Create Component From Existing")
cloned_fields = [
"agreement",
"merge_style",
"commit_message",
"add_message",
"delete_message",
"merge_message",
"addon_message",
"pull_message",
]
for field in cloned_fields:
self.assertEqual(
getattr(new_component, field), getattr(self.component, field)
)

@modify_settings(INSTALLED_APPS={"remove": "weblate.billing"})
def test_create_component_branch_fail(self) -> None:
Expand Down
12 changes: 4 additions & 8 deletions weblate/trans/tests/test_reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
from django.urls import reverse
from django.utils import timezone

from weblate.trans.models.category import Category
from weblate.trans.tests.test_views import ViewTestCase
from weblate.trans.views.reports import generate_counts, generate_credits

Expand Down Expand Up @@ -397,15 +396,12 @@ def get_kwargs(self):
class ReportsCategoryTest(ReportsComponentTest):
def setUp(self) -> None:
super().setUp()
self.category = self.create_category()
self.setup_category()

def create_category(self) -> None:
category = Category.objects.create(
name="test category", slug="test-category", project=self.project
)
self.component.category = category
def setup_category(self) -> None:
self.component.category = self.create_category(project=self.project)
self.component.save()
return category
self.category = self.component.category

def get_kwargs(self) -> dict[str, tuple]:
return {"path": self.category.get_url_path()}
8 changes: 7 additions & 1 deletion weblate/trans/tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from weblate.auth.models import User
from weblate.configuration.models import Setting, SettingCategory
from weblate.formats.models import FILE_FORMATS
from weblate.trans.models import Component, Project
from weblate.trans.models import Category, Component, Project
from weblate.utils.files import remove_tree
from weblate.vcs.models import VCS_REGISTRY

Expand Down Expand Up @@ -166,6 +166,12 @@ def create_project(self, **kwargs):
self.addCleanup(remove_tree, project.full_path, True)
return project

def create_category(self, project, **kwargs):
"""Create test category."""
return Category.objects.create(
name="Test category", slug="test-category", project=project, **kwargs
)

def format_local_path(self, path):
"""Format path for local access to the repository."""
if sys.platform != "win32":
Expand Down
Loading
Loading