Skip to content

Commit

Permalink
Award badges on file uploads (mozilla#3522)
Browse files Browse the repository at this point in the history
  • Loading branch information
harmitgoswami authored Jan 15, 2025
1 parent 402bf74 commit 89f390a
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 29 deletions.
13 changes: 12 additions & 1 deletion pontoon/base/views.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
import logging
import re

Expand Down Expand Up @@ -805,8 +806,18 @@ def upload(request):

upload = request.FILES["uploadfile"]
try:
import_uploaded_file(project, locale, res_path, upload, request.user)
badge_name, badge_level = import_uploaded_file(
project, locale, res_path, upload, request.user
)
messages.success(request, "Translations updated from uploaded file.")
if badge_name:
message = json.dumps(
{
"name": badge_name,
"level": badge_level,
}
)
messages.info(request, message, extra_tags="badge")
except Exception as error:
messages.error(request, str(error))
else:
Expand Down
26 changes: 21 additions & 5 deletions pontoon/sync/core/translations_from_repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
)
from pontoon.checks import DB_FORMATS
from pontoon.checks.utils import bulk_run_checks
from pontoon.messaging.notifications import send_badge_notification
from pontoon.sync.core.checkout import Checkout, Checkouts
from pontoon.sync.core.paths import UploadPaths
from pontoon.sync.formats import parse
Expand Down Expand Up @@ -76,12 +77,13 @@ def sync_translations_from_repo(

def write_db_updates(
project: Project, updates: Updates, user: User | None, now: datetime
) -> None:
updated_translations, new_translations = update_db_translations(
project, updates, user, now
) -> tuple[str, int]:
badge_name, badge_level, updated_translations, new_translations = (
update_db_translations(project, updates, user, now)
)
add_failed_checks(new_translations)
add_translation_memory_entries(project, new_translations + updated_translations)
return badge_name, badge_level


def delete_removed_bilingual_resources(
Expand Down Expand Up @@ -460,10 +462,24 @@ def update_db_translations(
f"[{project.slug}] Created {str_n_translations(created)} from repo changes"
)

badge_name = ""
badge_level = 0
if actions:
ActionLog.objects.bulk_create(actions)
translation_before_level = log_user.badges_translation_level
review_before_level = log_user.badges_review_level

return created, list(suggestions.values())
ActionLog.objects.bulk_create(actions)
if log_user.username != "pontoon-sync":
if log_user.badges_translation_level > translation_before_level:
badge_name = "Translation Champion"
badge_level = log_user.badges_translation_level
send_badge_notification(log_user, badge_name, badge_level)
if log_user.badges_review_level > review_before_level:
badge_name = "Review Master"
badge_level = log_user.badges_review_level
send_badge_notification(log_user, badge_name, badge_level)

return badge_name, badge_level, created, list(suggestions.values())


def str_n_translations(n: int | Sized) -> str:
Expand Down
3 changes: 2 additions & 1 deletion pontoon/sync/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ def import_uploaded_file(
)
if updates:
now = timezone.now()
write_db_updates(project, updates, user, now)
badge_name, badge_level = write_db_updates(project, updates, user, now)
update_stats(project)
ChangedEntityLocale.objects.bulk_create(
(
Expand All @@ -105,5 +105,6 @@ def import_uploaded_file(
),
ignore_conflicts=True,
)
return badge_name, badge_level
else:
raise Exception("Upload failed.")
10 changes: 5 additions & 5 deletions translate/src/context/BadgeTooltip.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createContext, useEffect, useState } from 'react';
import { Localized } from '@fluent/react';
import { createContext } from 'react';
import { useNotifications } from '~/hooks/useNotifications';

export type BadgeTooltipMessage = Readonly<{
badgeName: string | null;
Expand All @@ -19,11 +19,11 @@ export function BadgeTooltipProvider({
}: {
children: React.ReactElement;
}) {
const [message, setMessage] = useState<BadgeTooltipMessage | null>(null);
const { badgeMessage, setBadgeMessage } = useNotifications();

return (
<BadgeTooltipMessage.Provider value={message}>
<ShowBadgeTooltip.Provider value={(tooltip) => setMessage(tooltip)}>
<BadgeTooltipMessage.Provider value={badgeMessage}>
<ShowBadgeTooltip.Provider value={(tooltip) => setBadgeMessage(tooltip)}>
{children}
</ShowBadgeTooltip.Provider>
</BadgeTooltipMessage.Provider>
Expand Down
20 changes: 3 additions & 17 deletions translate/src/context/Notification.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createContext, useEffect, useState } from 'react';
import { createContext } from 'react';
import { useNotifications } from '~/hooks/useNotifications';

type NotificationType = 'debug' | 'error' | 'info' | 'success' | 'warning';

Expand All @@ -20,22 +21,7 @@ export function NotificationProvider({
}: {
children: React.ReactElement;
}) {
const [message, setMessage] = useState<NotificationMessage | null>(null);

// If there's a notification in the DOM set by Django, show it.
// Note that we only show it once, and only when the UI has already
// been rendered, to make sure users do see it.
useEffect(() => {
const rootElt = document.getElementById('root');
if (rootElt?.dataset.notifications) {
const notifications = JSON.parse(rootElt.dataset.notifications);
if (notifications.length > 0) {
// Our notification system only supports showing one notification
// for the moment, so we only add the first notification here.
setMessage(notifications[0]);
}
}
}, []);
const { message, setMessage } = useNotifications();

return (
<NotificationMessage.Provider value={message}>
Expand Down
49 changes: 49 additions & 0 deletions translate/src/hooks/useNotifications.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { useState, useEffect } from 'react';
import { NotificationMessage } from '~/context/Notification';
import { BadgeTooltipMessage } from '~/context/BadgeTooltip';

export function useNotifications() {
const [message, setMessage] = useState<NotificationMessage | null>(null);
const [badgeMessage, setBadgeMessage] = useState<BadgeTooltipMessage | null>(
null,
);

// If there's a notification in the DOM set by Django, show it.
// Note that we only show it once, and only when the UI has already
// been rendered, to make sure users do see it.
useEffect(() => {
const rootElt = document.getElementById('root');
if (rootElt?.dataset.notifications) {
const notifications = JSON.parse(rootElt.dataset.notifications);
if (notifications.length > 0) {
// Extra tags from the Django messages framework are combined
// with the level into a single string as notification.type
const generalNotification = notifications.find(
(notification: { type: string }) =>
notification.type !== 'badge info',
);
const badgeNotification = notifications.find(
(notification: { type: string }) =>
notification.type === 'badge info',
);

if (generalNotification) {
setMessage({
type: generalNotification.type,
content: generalNotification.content,
});
}

if (badgeNotification) {
const badgeData = JSON.parse(badgeNotification.content);
setBadgeMessage({
badgeName: badgeData.name || null,
badgeLevel: badgeData.level || null,
});
}
}
}
}, []);

return { message, setMessage, badgeMessage, setBadgeMessage };
}

0 comments on commit 89f390a

Please sign in to comment.