-
-
Notifications
You must be signed in to change notification settings - Fork 960
/
category.py
233 lines (198 loc) · 7.5 KB
/
category.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# Copyright © Michal Čihař <michal@weblate.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import annotations
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import Q
from django.utils.functional import cached_property
from django.utils.translation import gettext, gettext_lazy
from weblate.lang.models import Language
from weblate.trans.defines import CATEGORY_DEPTH, COMPONENT_NAME_LENGTH
from weblate.trans.mixins import CacheKeyMixin, ComponentCategoryMixin, PathMixin
from weblate.trans.models.change import Change
from weblate.utils.stats import CategoryStats
from weblate.utils.validators import validate_slug
class CategoryQuerySet(models.QuerySet):
def search(self, query: str):
return self.filter(
Q(name__icontains=query) | Q(slug__icontains=query)
).select_related(
"project",
"category__project",
"category__category",
"category__category__project",
"category__category__category",
"category__category__category__project",
)
def order(self):
return self.order_by("name")
class Category(models.Model, PathMixin, CacheKeyMixin, ComponentCategoryMixin):
name = models.CharField(
verbose_name=gettext_lazy("Category name"),
max_length=COMPONENT_NAME_LENGTH,
help_text=gettext_lazy("Display name"),
)
slug = models.SlugField(
verbose_name=gettext_lazy("URL slug"),
max_length=COMPONENT_NAME_LENGTH,
help_text=gettext_lazy("Name used in URLs and filenames."),
validators=[validate_slug],
)
project = models.ForeignKey(
"trans.Project",
verbose_name=gettext_lazy("Project"),
on_delete=models.deletion.CASCADE,
)
category = models.ForeignKey(
"trans.Category",
verbose_name=gettext_lazy("Category"),
on_delete=models.deletion.CASCADE,
null=True,
blank=True,
related_name="category_set",
)
is_lockable = False
remove_permission = "project.edit"
settings_permission = "project.edit"
objects = CategoryQuerySet.as_manager()
class Meta:
app_label = "trans"
verbose_name = "Category"
verbose_name_plural = "Categories"
constraints = [
models.UniqueConstraint(
name="category_slug_unique",
fields=["project", "category", "slug"],
nulls_distinct=False,
),
models.UniqueConstraint(
name="category_name_unique",
fields=["project", "category", "name"],
nulls_distinct=False,
),
]
def __str__(self) -> str:
return f"{self.category or self.project}/{self.name}"
def save(self, *args, **kwargs) -> None:
old = None
if self.id:
old = Category.objects.get(pk=self.id)
self.generate_changes(old)
self.check_rename(old)
self.create_path()
super().save(*args, **kwargs)
if old:
# Update linked repository references
if (
old.slug != self.slug
or old.project != self.project
or old.category != self.category
):
for component in self.all_components.exclude(component=None):
component.linked_childs.update(repo=component.get_repo_link_url())
# Move to a different project
if old.project != self.project:
self.move_to_project(self.project)
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.stats = CategoryStats(self)
def move_to_project(self, project) -> None:
"""Trigger save with changed project on categories and components."""
for category in self.category_set.all():
category.project = project
category.save()
for component in self.component_set.all():
component.project = project
component.save()
def get_url_path(self):
parent = self.category or self.project
return (*parent.get_url_path(), self.slug)
def _get_childs_depth(self):
return 1 + max(
(child._get_childs_depth() for child in self.category_set.all()),
default=0,
)
def _get_parents_depth(self):
depth = 0
current = self
while current.category:
depth += 1
current = current.category
return depth
def _get_category_depth(self):
return (self._get_childs_depth() if self.pk else 1) + self._get_parents_depth()
@property
def can_add_category(self):
return self._get_parents_depth() + 1 < CATEGORY_DEPTH
def clean(self) -> None:
# Validate maximal nesting depth
depth = self._get_category_depth()
if depth > CATEGORY_DEPTH:
raise ValidationError(
{"category": gettext("Too deep nesting of categories!")}
)
if self.category and self.category.project != self.project:
raise ValidationError(
{"category": gettext("Parent category has to be in the same project!")}
)
if self.category and self == self.category:
raise ValidationError(
{"category": gettext("Parent category has to be different!")}
)
# Validate category/component name uniqueness at given level
self.clean_unique_together()
if self.id:
old = Category.objects.get(pk=self.id)
self.check_rename(old, validate=True)
def get_child_components_access(self, user):
"""List child components."""
return self.component_set.filter_access(user).prefetch().order()
@cached_property
def languages(self):
"""Return list of all languages used in project."""
return (
Language.objects.filter(
translation__component_id__in=self.all_component_ids
)
.distinct()
.order()
)
@cached_property
def all_components(self):
from weblate.trans.models import Component
return Component.objects.filter(
Q(category=self)
| Q(category__category=self)
| Q(category__category__category=self)
)
@cached_property
def all_component_ids(self):
return set(self.all_components.values_list("pk", flat=True))
def generate_changes(self, old) -> None:
def getvalue(base, attribute):
result = getattr(base, attribute)
if result is None:
return ""
# Use slug for Category instances
return getattr(result, "slug", result)
tracked = (
("slug", Change.ACTION_RENAME_CATEGORY),
("category", Change.ACTION_MOVE_CATEGORY),
("project", Change.ACTION_MOVE_CATEGORY),
)
for attribute, action in tracked:
old_value = getvalue(old, attribute)
current_value = getvalue(self, attribute)
if old_value != current_value:
self.project.change_set.create(
action=action,
old=old_value,
target=current_value,
user=self.acting_user,
)
@cached_property
def source_language_ids(self):
return set(
self.all_components.values_list("source_language_id", flat=True).distinct()
)