|
1 | 1 | from dirtyfields import DirtyFieldsMixin |
2 | 2 |
|
3 | 3 | from django.contrib.postgres.fields import ArrayField |
4 | | -from django.db import models |
| 4 | +from django.db import IntegrityError, models, transaction |
5 | 5 | from django.db.models import Count, Q |
6 | 6 | from django.utils import timezone |
7 | 7 |
|
@@ -249,6 +249,8 @@ def save(self, failed_checks=None, *args, **kwargs): |
249 | 249 | from pontoon.base.models.translated_resource import TranslatedResource |
250 | 250 | from pontoon.base.models.translation_memory import TranslationMemoryEntry |
251 | 251 |
|
| 252 | + stats_before = self._get_entity_stats() |
| 253 | + |
252 | 254 | super().save(*args, **kwargs) |
253 | 255 |
|
254 | 256 | project = self.entity.resource.project |
@@ -320,7 +322,65 @@ def save(self, failed_checks=None, *args, **kwargs): |
320 | 322 | save_failed_checks(self, failed_checks) |
321 | 323 |
|
322 | 324 | # Update stats AFTER changing approval status. |
323 | | - translatedresource.calculate_stats() |
| 325 | + # Use entity-scoped aggregate delta for performance. Fall back to a |
| 326 | + # full resource recount on IntegrityError. |
| 327 | + stats_after = self._get_entity_stats() |
| 328 | + try: |
| 329 | + with transaction.atomic(): |
| 330 | + translatedresource.adjust_stats(stats_before, stats_after, created) |
| 331 | + except IntegrityError: |
| 332 | + translatedresource.calculate_stats() |
| 333 | + |
| 334 | + def _get_entity_stats(self) -> dict[str, int]: |
| 335 | + """ |
| 336 | + Aggregate translation stats for this entity+locale pair. |
| 337 | + Used to compute before/after deltas in save() without scanning |
| 338 | + the entire resource. |
| 339 | + """ |
| 340 | + stats = Translation.objects.filter( |
| 341 | + entity=self.entity, |
| 342 | + locale=self.locale, |
| 343 | + ).aggregate( |
| 344 | + approved_count=Count( |
| 345 | + "pk", |
| 346 | + filter=Q(approved=True, errors__isnull=True, warnings__isnull=True), |
| 347 | + ), |
| 348 | + pretranslated_count=Count( |
| 349 | + "pk", |
| 350 | + filter=Q( |
| 351 | + pretranslated=True, errors__isnull=True, warnings__isnull=True |
| 352 | + ), |
| 353 | + ), |
| 354 | + errors_count=Count( |
| 355 | + "pk", |
| 356 | + distinct=True, |
| 357 | + filter=Q( |
| 358 | + Q(Q(approved=True) | Q(pretranslated=True) | Q(fuzzy=True)) |
| 359 | + & Q(errors__isnull=False) |
| 360 | + ), |
| 361 | + ), |
| 362 | + warnings_count=Count( |
| 363 | + "pk", |
| 364 | + distinct=True, |
| 365 | + filter=Q( |
| 366 | + Q(Q(approved=True) | Q(pretranslated=True) | Q(fuzzy=True)) |
| 367 | + & Q(warnings__isnull=False) |
| 368 | + ), |
| 369 | + ), |
| 370 | + unreviewed_count=Count( |
| 371 | + "pk", |
| 372 | + filter=Q( |
| 373 | + approved=False, rejected=False, pretranslated=False, fuzzy=False |
| 374 | + ), |
| 375 | + ), |
| 376 | + ) |
| 377 | + return { |
| 378 | + "approved": stats["approved_count"], |
| 379 | + "pretranslated": stats["pretranslated_count"], |
| 380 | + "errors": stats["errors_count"], |
| 381 | + "warnings": stats["warnings_count"], |
| 382 | + "unreviewed": stats["unreviewed_count"], |
| 383 | + } |
324 | 384 |
|
325 | 385 | def update_latest_translation(self): |
326 | 386 | """ |
|
0 commit comments