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

Merged
merged 25 commits into from
Nov 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
cb93a79
additional attributes in component duplication
gersona Aug 22, 2024
f08e572
additional "duplicate component" added to manage menu
gersona Aug 22, 2024
8efa10f
update tests
gersona Aug 23, 2024
70a7abc
update changelog
gersona Aug 23, 2024
ad3a863
migration file generated for component model
gersona Aug 23, 2024
5600f75
fix test coverage
gersona Aug 26, 2024
40e65ba
Merge remote-tracking branch 'origin/main' into 10896_duplicate_compo…
gersona Aug 26, 2024
ae3db88
Merge remote-tracking branch 'upstream/main' into 10896_duplicate_com…
gersona Sep 30, 2024
2888e82
copy duplicate fields from source component
gersona Oct 8, 2024
d6d899a
Merge remote-tracking branch 'upstream/main' into 10896_duplicate_com…
gersona Oct 8, 2024
66a9fa6
fix migration file conflict
gersona Oct 8, 2024
2e87e5c
Merge branch 'WeblateOrg:main' into 10896_duplicate_component
gersona Oct 8, 2024
fae4983
Merge remote-tracking branch 'upstream/main' into 10896_duplicate_com…
gersona Oct 14, 2024
3c097fd
Merge remote-tracking branch 'upstream/main' into 10896_duplicate_com…
gersona Oct 15, 2024
bfd05be
Revert component fields to required
gersona Oct 15, 2024
15f42a3
Merge branch 'main' into 10896_duplicate_component
gersona Oct 17, 2024
eca49dd
Fix component filtering for an user
nijel Nov 6, 2024
5bc1132
Merge branch 'main' into 10896_duplicate_component
nijel Nov 6, 2024
9bf3c2c
Update weblate/trans/views/create.py
nijel Nov 6, 2024
7bf7152
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 6, 2024
0ac335a
Update weblate/templates/component.html
nijel Nov 6, 2024
f87fe7f
Merge branch 'main' into 10896_duplicate_component
nijel Nov 6, 2024
27ca649
Merge branch 'main' into 10896_duplicate_component
nijel Nov 6, 2024
8ea6e9b
Merge branch 'main' into 10896_duplicate_component
nijel Nov 6, 2024
08998b7
Merge branch 'main' into 10896_duplicate_component
nijel Nov 6, 2024
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 @@ -7,6 +7,7 @@ Not yet released.

**Improvements**

* A shortcut to duplicate a component is now available directly in the menu (:guilabel:`Manage` → :guilabel:`Duplicate Component`)
* Included username when generating :ref:`credits`.

**Bug fixes**
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" %}</a></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 @@ -1664,6 +1664,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
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 @@ -180,22 +182,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 @@ -399,15 +398,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
52 changes: 51 additions & 1 deletion weblate/trans/views/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,22 @@ def form_valid(self, form):
form.instance.manage_units = (
bool(form.instance.template) or form.instance.file_format == "tbx"
)
if self.duplicate_existing_component and (
source_component := form.cleaned_data["source_component"]
):
fields_to_duplicate = [
"agreement",
"merge_style",
"commit_message",
"add_message",
"delete_message",
"merge_message",
"addon_message",
"pull_message",
]
for field in fields_to_duplicate:
setattr(form.instance, field, getattr(source_component, field))

result = super().form_valid(form)
self.object.post_create(self.request.user)
return result
Expand Down Expand Up @@ -325,6 +341,12 @@ def get_form(self, form_class=None, empty=False):
if self.selected_category:
category_field.initial = self.selected_category
self.empty_form = False
if "source_component" in form.fields and self.duplicate_existing_component:
self.components = Component.objects.filter(
pk=self.duplicate_existing_component
)
form.fields["source_component"].queryset = self.components
form.initial["source_component"] = self.duplicate_existing_component
return form

def get_context_data(self, **kwargs):
Expand Down Expand Up @@ -361,6 +383,11 @@ def fetch_params(self, request: AuthenticatedHttpRequest) -> None:
if field in request.GET:
self.initial[field] = request.GET[field]

try:
self.duplicate_existing_component = int(request.GET.get("source_component"))
except (ValueError, TypeError):
self.duplicate_existing_component = None

def has_all_fields(self):
return self.stage == "init" and all(
field in self.request.GET for field in self.basic_fields
Expand Down Expand Up @@ -437,6 +464,7 @@ class CreateComponentSelection(CreateComponent):

components: ComponentQuerySet
origin: None | str = None
duplicate_existing_component = None

@cached_property
def branch_data(self):
Expand Down Expand Up @@ -468,6 +496,20 @@ def fetch_params(self, request: AuthenticatedHttpRequest) -> None:
self.components = self.components.filter(project__pk=self.selected_project)
self.origin = request.POST.get("origin")

try:
self.duplicate_existing_component = int(request.GET.get("component"))
except (ValueError, TypeError):
self.duplicate_existing_component = None
self.initial = {}
if self.duplicate_existing_component:
source_component = Component.objects.get(
pk=self.duplicate_existing_component
)
self.initial |= {
"component": source_component,
"is_glossary": source_component.is_glossary,
}

def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs)
kwargs["components"] = self.components
Expand Down Expand Up @@ -498,6 +540,10 @@ def get_form(self, form_class=None, empty=False):
).order_project()
form.branch_data = self.branch_data
elif isinstance(form, ComponentSelectForm):
if self.duplicate_existing_component:
self.components |= Component.objects.filter_access(
self.request.user
).filter(pk=self.duplicate_existing_component)
form.fields["component"].queryset = self.components
return form

Expand All @@ -523,12 +569,16 @@ def form_valid(self, form):
component = form.cleaned_data["component"]
if self.origin == "existing":
return self.redirect_create(
repo=component.get_repo_link_url(),
repo=component.repo or component.get_repo_link_url(),
project=component.project.pk,
category=component.category.pk if component.category else "",
name=form.cleaned_data["name"],
slug=form.cleaned_data["slug"],
is_glossary=form.cleaned_data["is_glossary"],
vcs=component.vcs,
source_language=component.source_language.pk,
license=component.license,
source_component=component.pk,
)
if self.origin == "branch":
form.instance.save()
Expand Down
Loading