diff --git a/pontoon/base/models/user.py b/pontoon/base/models/user.py index 329f24c4ce..43053aec9c 100644 --- a/pontoon/base/models/user.py +++ b/pontoon/base/models/user.py @@ -275,6 +275,24 @@ def badges_promotion_count(self): ) +@property +def badges_translation_level(self): + thresholds = settings.BADGES_TRANSLATION_THRESHOLDS + for level in range(len(thresholds) - 1): + if thresholds[level] <= self.badges_translation_count < thresholds[level + 1]: + return level + 1 + return 0 + + +@property +def badges_review_level(self): + thresholds = settings.BADGES_REVIEW_THRESHOLDS + for level in range(len(thresholds) - 1): + if thresholds[level] <= self.badges_review_count < thresholds[level + 1]: + return level + 1 + return 0 + + @property def top_contributed_locale(self): """Locale the user has made the most contributions to.""" @@ -523,6 +541,8 @@ def latest_action(self): User.add_to_class("badges_translation_count", badges_translation_count) User.add_to_class("badges_review_count", badges_review_count) User.add_to_class("badges_promotion_count", badges_promotion_count) +User.add_to_class("badges_translation_level", badges_translation_level) +User.add_to_class("badges_review_level", badges_review_level) User.add_to_class("has_approved_translations", has_approved_translations) User.add_to_class("top_contributed_locale", top_contributed_locale) User.add_to_class("can_translate", can_translate) diff --git a/pontoon/batch/actions.py b/pontoon/batch/actions.py index 2078eb2f86..e01a3ced5f 100644 --- a/pontoon/batch/actions.py +++ b/pontoon/batch/actions.py @@ -6,6 +6,7 @@ TranslationMemoryEntry, ) from pontoon.batch import utils +from pontoon.messaging.notifications import send_badge_notification def batch_action_template(form, user, translations, locale): @@ -69,6 +70,8 @@ def approve_translations(form, user, translations, locale): locale, ) + before_level = user.badges_review_level + # Log approving actions actions_to_log = [ ActionLog( @@ -80,6 +83,14 @@ def approve_translations(form, user, translations, locale): ] ActionLog.objects.bulk_create(actions_to_log) + # Send Review Master Badge notification information + after_level = user.badges_review_level + badge_update = {} + if after_level > before_level: + badge_update["level"] = after_level + badge_update["name"] = "Review Master" + send_badge_notification(user, badge_update["name"], badge_update["level"]) + # Approve translations. translations.update( approved=True, @@ -99,6 +110,7 @@ def approve_translations(form, user, translations, locale): "latest_translation_pk": latest_translation_pk, "changed_translation_pks": changed_translation_pks, "invalid_translation_pks": invalid_translation_pks, + "badge_update": badge_update, } @@ -124,6 +136,8 @@ def reject_translations(form, user, translations, locale): ) TranslationMemoryEntry.objects.filter(translation__in=suggestions).delete() + before_level = user.badges_review_level + # Log rejecting actions actions_to_log = [ ActionLog( @@ -135,6 +149,14 @@ def reject_translations(form, user, translations, locale): ] ActionLog.objects.bulk_create(actions_to_log) + # Send Review Master Badge notification information + after_level = user.badges_review_level + badge_update = {} + if after_level > before_level: + badge_update["level"] = after_level + badge_update["name"] = "Review Master" + send_badge_notification(user, badge_update["name"], badge_update["level"]) + # Reject translations. suggestions.update( active=False, @@ -155,6 +177,7 @@ def reject_translations(form, user, translations, locale): "latest_translation_pk": None, "changed_translation_pks": [], "invalid_translation_pks": [], + "badge_update": badge_update, } @@ -216,6 +239,8 @@ def replace_translations(form, user, translations, locale): translations_to_create, ) + before_level = user.badges_translation_level + # Log creating actions actions_to_log = [ ActionLog( @@ -227,6 +252,14 @@ def replace_translations(form, user, translations, locale): ] ActionLog.objects.bulk_create(actions_to_log) + # Send Translation Champion Badge notification information + after_level = user.badges_translation_level + badge_update = {} + if after_level > before_level: + badge_update["level"] = after_level + badge_update["name"] = "Translation Champion" + send_badge_notification(user, badge_update["name"], badge_update["level"]) + changed_translation_pks = [c.pk for c in changed_translations] if changed_translation_pks: @@ -239,6 +272,7 @@ def replace_translations(form, user, translations, locale): "latest_translation_pk": latest_translation_pk, "changed_translation_pks": changed_translation_pks, "invalid_translation_pks": invalid_translation_pks, + "badge_update": badge_update, } diff --git a/pontoon/batch/tests/test_views.py b/pontoon/batch/tests/test_views.py index bef0b785fc..2a4b0a1834 100644 --- a/pontoon/batch/tests/test_views.py +++ b/pontoon/batch/tests/test_views.py @@ -144,6 +144,7 @@ def test_batch_approve_valid_translations( assert response.json() == { "count": 1, "invalid_translation_count": 0, + "badge_update": {}, } translation_dtd_unapproved.refresh_from_db() @@ -170,6 +171,7 @@ def test_batch_approve_invalid_translations( assert response.json() == { "count": 0, "invalid_translation_count": 1, + "badge_update": {}, } translation_dtd_invalid_unapproved.refresh_from_db() @@ -195,6 +197,7 @@ def test_batch_find_and_replace_valid_translations( assert response.json() == { "count": 1, "invalid_translation_count": 0, + "badge_update": {}, } translation = translation_dtd_unapproved.entity.translation_set.last() @@ -224,6 +227,7 @@ def test_batch_find_and_replace_invalid_translations( assert response.json() == { "count": 0, "invalid_translation_count": 1, + "badge_update": {}, } translation = translation_dtd_unapproved.entity.translation_set.last() diff --git a/pontoon/batch/views.py b/pontoon/batch/views.py index 13c13f1ed8..a2ea66c68a 100644 --- a/pontoon/batch/views.py +++ b/pontoon/batch/views.py @@ -114,7 +114,11 @@ def batch_edit_translations(request): invalid_translation_count = len(action_status.get("invalid_translation_pks", [])) if action_status["count"] == 0: return JsonResponse( - {"count": 0, "invalid_translation_count": invalid_translation_count} + { + "count": 0, + "invalid_translation_count": invalid_translation_count, + "badge_update": action_status["badge_update"], + } ) tr_pks = [tr.pk for tr in action_status["translated_resources"]] @@ -145,5 +149,6 @@ def batch_edit_translations(request): { "count": action_status["count"], "invalid_translation_count": invalid_translation_count, + "badge_update": action_status["badge_update"], } ) diff --git a/pontoon/translations/views.py b/pontoon/translations/views.py index e81e6773f1..5d7dd9d27f 100644 --- a/pontoon/translations/views.py +++ b/pontoon/translations/views.py @@ -175,7 +175,7 @@ def create_translation(request): # Send Translation Champion Badge notification information translation_count = user.badges_translation_count if translation_count in settings.BADGES_TRANSLATION_THRESHOLDS: - badge_name = "Translation Champion Badge" + badge_name = "Translation Champion" badge_level = ( settings.BADGES_TRANSLATION_THRESHOLDS.index(translation_count) + 1 ) @@ -323,7 +323,7 @@ def approve_translation(request): # Send Review Master Badge notification information review_count = user.badges_review_count if review_count in settings.BADGES_REVIEW_THRESHOLDS: - badge_name = "Review Master Badge" + badge_name = "Review Master" badge_level = settings.BADGES_REVIEW_THRESHOLDS.index(review_count) + 1 _add_badge_data(response_data, user, badge_name, badge_level) @@ -460,7 +460,7 @@ def reject_translation(request): # Send Review Master Badge notification information review_count = request.user.badges_review_count if review_count in settings.BADGES_REVIEW_THRESHOLDS: - badge_name = "Review Master Badge" + badge_name = "Review Master" badge_level = settings.BADGES_REVIEW_THRESHOLDS.index(review_count) + 1 _add_badge_data(response_data, request.user, badge_name, badge_level) diff --git a/translate/src/api/entity.ts b/translate/src/api/entity.ts index 19fadcbd49..5abd74efdc 100644 --- a/translate/src/api/entity.ts +++ b/translate/src/api/entity.ts @@ -8,6 +8,7 @@ import type { EntityTranslation, HistoryTranslation, } from './translation'; +import type { BatchBadgeUpdate } from '../modules/batchactions/actions'; /** * String that needs to be translated, along with its current metadata, @@ -42,7 +43,11 @@ export type EntitySiblings = { }; type BatchEditResponse = - | { count: number; invalid_translation_count?: number } + | { + count: number; + invalid_translation_count?: number; + badge_update?: BatchBadgeUpdate; + } | { error: true }; export async function batchEditEntities( diff --git a/translate/src/modules/batchactions/actions.ts b/translate/src/modules/batchactions/actions.ts index b5625699dc..a6d746c045 100644 --- a/translate/src/modules/batchactions/actions.ts +++ b/translate/src/modules/batchactions/actions.ts @@ -14,10 +14,16 @@ export const RESET_BATCHACTIONS_RESPONSE = 'batchactions/RESET_RESPONSE'; export const TOGGLE_BATCHACTIONS = 'batchactions/TOGGLE'; export const UNCHECK_BATCHACTIONS = 'batchactions/UNCHECK'; +export type BatchBadgeUpdate = { + name: string | null; + level: number | null; +}; + export type ResponseType = { action: string; changedCount: number | null | undefined; invalidCount: number | null | undefined; + badgeUpdate: BatchBadgeUpdate | null | undefined; error: boolean | null | undefined; }; @@ -117,6 +123,10 @@ export const performAction = location: Location, action: 'approve' | 'reject' | 'replace', entityIds: number[], + showBadgeTooltip: (tooltip: { + badgeName: string | null; + badgeLevel: number | null; + }) => void, find?: string, replace?: string, ) => @@ -134,6 +144,10 @@ export const performAction = const response: ResponseType = { changedCount: 0, invalidCount: 0, + badgeUpdate: { + name: '', + level: 0, + }, error: false, action, }; @@ -141,6 +155,14 @@ export const performAction = if ('count' in data) { response.changedCount = data.count; response.invalidCount = data.invalid_translation_count; + response.badgeUpdate = data.badge_update; + + if (response.badgeUpdate?.level && response.badgeUpdate?.level > 0) { + showBadgeTooltip({ + badgeName: response.badgeUpdate.name, + badgeLevel: response.badgeUpdate.level, + }); + } if (data.count > 0) { dispatch(updateUI(location, entityIds)); diff --git a/translate/src/modules/batchactions/components/BatchActions.tsx b/translate/src/modules/batchactions/components/BatchActions.tsx index 13cc8121b5..de01d064b7 100644 --- a/translate/src/modules/batchactions/components/BatchActions.tsx +++ b/translate/src/modules/batchactions/components/BatchActions.tsx @@ -2,6 +2,7 @@ import { Localized } from '@fluent/react'; import React, { useCallback, useContext, useEffect, useRef } from 'react'; import { Location } from '~/context/Location'; +import { ShowBadgeTooltip } from '~/context/BadgeTooltip'; import { useAppDispatch, useAppSelector } from '~/hooks'; import { performAction, resetSelection, selectAll } from '../actions'; @@ -18,6 +19,7 @@ import { ReplaceAll } from './ReplaceAll'; export function BatchActions(): React.ReactElement<'div'> { const batchactions = useAppSelector((state) => state[BATCHACTIONS]); const location = useContext(Location); + const showBadgeTooltip = useContext(ShowBadgeTooltip); const dispatch = useAppDispatch(); const find = useRef(null); @@ -43,13 +45,27 @@ export function BatchActions(): React.ReactElement<'div'> { const approveAll = useCallback(() => { if (!batchactions.requestInProgress) { - dispatch(performAction(location, 'approve', batchactions.entities)); + dispatch( + performAction( + location, + 'approve', + batchactions.entities, + showBadgeTooltip, + ), + ); } }, [location, batchactions]); const rejectAll = useCallback(() => { if (!batchactions.requestInProgress) { - dispatch(performAction(location, 'reject', batchactions.entities)); + dispatch( + performAction( + location, + 'reject', + batchactions.entities, + showBadgeTooltip, + ), + ); } }, [location, batchactions]); @@ -67,6 +83,7 @@ export function BatchActions(): React.ReactElement<'div'> { location, 'replace', batchactions.entities, + showBadgeTooltip, encodeURIComponent(fv), encodeURIComponent(rv), ), diff --git a/translate/src/modules/editor/hooks/useSendTranslation.ts b/translate/src/modules/editor/hooks/useSendTranslation.ts index ebef2ad3aa..aae03f6e55 100644 --- a/translate/src/modules/editor/hooks/useSendTranslation.ts +++ b/translate/src/modules/editor/hooks/useSendTranslation.ts @@ -87,9 +87,10 @@ export function useSendTranslation(): (ignoreWarnings?: boolean) => void { ); const badgeLevel = content.badge_update?.level; - if (badgeLevel) { + const badgeName = content.badge_update?.name; + if (badgeName && badgeLevel) { showBadgeTooltip({ - badgeName: 'Translation Champion', + badgeName: badgeName, badgeLevel: badgeLevel, }); } diff --git a/translate/src/modules/editor/hooks/useUpdateTranslationStatus.ts b/translate/src/modules/editor/hooks/useUpdateTranslationStatus.ts index 081ed47e74..2a00c46ff5 100644 --- a/translate/src/modules/editor/hooks/useUpdateTranslationStatus.ts +++ b/translate/src/modules/editor/hooks/useUpdateTranslationStatus.ts @@ -93,9 +93,10 @@ export function useUpdateTranslationStatus( // Check for update in badge level const badgeLevel = results.badge_update?.level; - if (badgeLevel) { + const badgeName = results.badge_update?.name; + if (badgeName && badgeLevel) { showBadgeTooltip({ - badgeName: 'Review Master', + badgeName: badgeName, badgeLevel: badgeLevel, }); }