From 34e0a5e29def7f845412edd4fd651c3d36efec05 Mon Sep 17 00:00:00 2001 From: Harmit Goswami Date: Wed, 18 Dec 2024 14:00:18 -0500 Subject: [PATCH] Initial commit for counting batch actions --- pontoon/batch/actions.py | 54 +++++++++++++++++++ pontoon/batch/tests/test_views.py | 4 ++ pontoon/batch/views.py | 7 ++- translate/src/api/entity.ts | 7 ++- translate/src/modules/batchactions/actions.ts | 22 ++++++++ .../batchactions/components/BatchActions.tsx | 21 +++++++- 6 files changed, 111 insertions(+), 4 deletions(-) diff --git a/pontoon/batch/actions.py b/pontoon/batch/actions.py index 2078eb2f86..64bec22aec 100644 --- a/pontoon/batch/actions.py +++ b/pontoon/batch/actions.py @@ -1,3 +1,4 @@ +from django.conf import settings from django.utils import timezone from pontoon.actionlog.models import ActionLog @@ -6,6 +7,14 @@ TranslationMemoryEntry, ) from pontoon.batch import utils +from pontoon.messaging.notifications import send_badge_notification + + +def _get_badge_level(thresholds, action_count): + for level in range(len(thresholds) - 1): + if thresholds[level] <= action_count < thresholds[level + 1]: + return level + 1 + return 0 def batch_action_template(form, user, translations, locale): @@ -69,6 +78,10 @@ def approve_translations(form, user, translations, locale): locale, ) + before_level = _get_badge_level( + settings.BADGES_REVIEW_THRESHOLDS, user.badges_review_count + ) + # Log approving actions actions_to_log = [ ActionLog( @@ -80,6 +93,16 @@ def approve_translations(form, user, translations, locale): ] ActionLog.objects.bulk_create(actions_to_log) + # Send Review Master Badge notification information + after_level = _get_badge_level( + settings.BADGES_REVIEW_THRESHOLDS, user.badges_review_count + ) + badge_update = {} + if after_level > before_level: + badge_update["level"] = after_level + badge_update["name"] = "Review Master Badge" + send_badge_notification(user, badge_update["name"], badge_update["level"]) + # Approve translations. translations.update( approved=True, @@ -99,6 +122,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 +148,10 @@ def reject_translations(form, user, translations, locale): ) TranslationMemoryEntry.objects.filter(translation__in=suggestions).delete() + before_level = _get_badge_level( + settings.BADGES_REVIEW_THRESHOLDS, user.badges_review_count + ) + # Log rejecting actions actions_to_log = [ ActionLog( @@ -135,6 +163,16 @@ def reject_translations(form, user, translations, locale): ] ActionLog.objects.bulk_create(actions_to_log) + # Send Review Master Badge notification information + after_level = _get_badge_level( + settings.BADGES_REVIEW_THRESHOLDS, user.badges_review_count + ) + badge_update = {} + if after_level > before_level: + badge_update["level"] = after_level + badge_update["name"] = "Review Master Badge" + send_badge_notification(user, badge_update["name"], badge_update["level"]) + # Reject translations. suggestions.update( active=False, @@ -155,6 +193,7 @@ def reject_translations(form, user, translations, locale): "latest_translation_pk": None, "changed_translation_pks": [], "invalid_translation_pks": [], + "badge_update": badge_update, } @@ -216,6 +255,10 @@ def replace_translations(form, user, translations, locale): translations_to_create, ) + before_level = _get_badge_level( + settings.BADGES_TRANSLATION_THRESHOLDS, user.badges_translation_count + ) + # Log creating actions actions_to_log = [ ActionLog( @@ -227,6 +270,16 @@ def replace_translations(form, user, translations, locale): ] ActionLog.objects.bulk_create(actions_to_log) + # Send Translation Champion Badge notification information + after_level = _get_badge_level( + settings.BADGES_TRANSLATION_THRESHOLDS, user.badges_translation_count + ) + badge_update = {} + if after_level > before_level: + badge_update["level"] = after_level + badge_update["name"] = "Translation Champion Badge" + 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 +292,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/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), ),