Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion documentation/docs/dev/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ Optional. Controls if the database needs a secure connection with the
app. Default value is `True`.

`DEFAULT_FROM_EMAIL`
Optional. Default email address to send emails from. Default value: `Pontoon <pontoon@hostname>`.
Optional. Default email address to send emails from. Default value: `Pontoon <no-reply@pontoon.mozilla.com>`.

`DISABLE_COLLECTSTATIC`
Disables running `./manage.py collectstatic` during the build. Should be
Expand Down
2 changes: 1 addition & 1 deletion pontoon/administration/static/css/admin_project.css
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ form .controls .button.manage-resources[disabled]:hover {

form a:link,
form a:visited {
color: var(--status-translated-alt);
color: var(--status-translated);
float: right;
text-transform: uppercase;
}
Expand Down
21 changes: 21 additions & 0 deletions pontoon/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -619,3 +619,24 @@ class UploadSuggestionsResponseSerializer(serializers.Serializer):
undefined_keys = undefined_keys_field()
undefined_keys_count = undefined_keys_count_field()
badge_updates = badge_updates_field()


class TranslationCommentRequestSerializer(serializers.Serializer):
"""Request to post a comment on a translation (e.g. LLM-based LQA)."""

translation = serializers.IntegerField(
help_text="Primary key of the translation to comment on."
)
content = serializers.CharField(
help_text="Comment content (HTML sanitized on save)."
)


class TranslationCommentResponseSerializer(serializers.Serializer):
"""Created translation comment."""

id = serializers.IntegerField(help_text="Primary key of the created comment.")
translation = serializers.IntegerField(
help_text="Primary key of the commented translation."
)
content = serializers.CharField(help_text="Sanitized comment content.")
5 changes: 5 additions & 0 deletions pontoon/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@
views.UploadSuggestionsView.as_view(),
name="upload-suggestions",
),
path(
"comments/translations/",
views.TranslationCommentView.as_view(),
name="comment-translation",
),
path(
"terminology/matches/",
views.TermMatchListView.as_view(),
Expand Down
63 changes: 63 additions & 0 deletions pontoon/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@
NestedProjectSerializer,
PretranslationResponseSerializer,
TermSerializer,
TranslationCommentRequestSerializer,
TranslationCommentResponseSerializer,
TranslationMemorySerializer,
UploadPretranslationsResponseSerializer,
UploadSuggestionsResponseSerializer,
Expand Down Expand Up @@ -1014,3 +1016,64 @@ def post(self, request):
**self.badge_updates(badges),
}
)


class TranslationCommentView(APIView):
permission_classes = [IsAuthenticated]

@extend_schema(
request=TranslationCommentRequestSerializer,
responses={
201: TranslationCommentResponseSerializer,
400: OpenApiResponse(description="Invalid translation pk or content."),
403: OpenApiResponse(description="Authentication required."),
404: OpenApiResponse(description="Unknown translation."),
},
description=(
"Post a comment to a translation, e.g. as part of LLM-based LQA. "
"Content is HTML-sanitized, an action-log entry is recorded, and "
"translation comment notifications are sent, mirroring the UI."
),
)
def post(self, request):
from pontoon.actionlog.utils import log_action
from pontoon.base.models import Comment
from pontoon.base.utils import sanitize_html
from pontoon.base.views import _send_add_comment_notifications

serializer = TranslationCommentRequestSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
translation = get_object_or_404(
Translation.objects.select_related("entity__resource__project", "locale"),
pk=serializer.validated_data["translation"],
)
content = sanitize_html(serializer.validated_data["content"])
if not content.strip():
raise ValidationError({"content": ["This field is required."]})

with transaction.atomic():
comment = Comment.objects.create(
author=request.user, translation=translation, content=content
)
log_action(
ActionLog.ActionType.COMMENT_ADDED,
request.user,
translation=translation,
)

_send_add_comment_notifications(
request.user,
content,
translation.entity,
translation.locale,
translation,
)

return Response(
{
"id": comment.pk,
"translation": translation.pk,
"content": comment.content,
},
status=status.HTTP_201_CREATED,
)
36 changes: 21 additions & 15 deletions pontoon/base/models/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,25 +70,31 @@ def reset_active_translation(self, locale: Locale) -> dict[str, Any] | None:

Return data for the active translation if it exists, or None otherwise.
"""
from pontoon.base.models.translation import Translation

translations = self.translation_set.filter(locale=locale)
translations.update(active=False)
from django.db import transaction

candidates = translations.filter(rejected=False).order_by(
"-approved", "-pretranslated", "-fuzzy", "-date"
)
from pontoon.base.models.translation import Translation

if candidates:
active_translation = candidates[0]
active_translation.active = True
with transaction.atomic():
translations = self.translation_set.select_for_update().filter(
locale=locale
)
translations.update(active=False)

# Do not trigger the overridden Translation.save() method
super(Translation, active_translation).save(update_fields=["active"])
candidates = translations.filter(rejected=False).order_by(
"-approved", "-pretranslated", "-fuzzy", "-date"
)

return active_translation.serialize()
else:
return None
if not candidates:
return None

active_pk = candidates[0].pk
# Belt-and-braces: clear any duplicate active rows activated
# concurrently or left over from before the unique constraint
# (see #2284), then activate the winner via queryset update to
# stay inside the same transaction.
translations.exclude(pk=active_pk).filter(active=True).update(active=False)
translations.filter(pk=active_pk).update(active=True)
return Translation.objects.get(pk=active_pk).serialize()

def reset_term_translation(self, locale):
"""
Expand Down
13 changes: 10 additions & 3 deletions pontoon/base/static/css/dark-theme.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
.dark-theme {
/* Main */
--main-border-1: #4d5967;
--control-border: #4d5967;
--focus-ring: #4a9ccc;
--moz-logo: url(../img/moz-logo-light.svg);

/* User banner and details */
Expand Down Expand Up @@ -36,6 +38,9 @@
--tooltip-color: #ffffff;
--tooltip-color-2: #aaaaaa;
--tooltip-border: #4d5967;
--tooltip-surface-background: #000000dd;
--tooltip-surface-color: #ffffff;
--tooltip-surface-color-2: #888888;

/* Homepage */
--homepage-background-image: url(../img/background.svg);
Expand All @@ -44,13 +49,10 @@

/* Translation status */
--status-translated: #7bc876;
--status-translated-alt: #7bc876;
--status-pretranslated: #c0ff00;
--status-pretranslated-alt: #c0ff00;
--status-warning: #ffa10f;
--status-error: #ff3366;
--status-missing: #5f7285;
--status-missing-alt: #5f7285;
--status-unreviewed: #4fc4f6;
--status-fuzzy: #fed271;

Expand All @@ -60,6 +62,7 @@
--translation-color: #ffffff;
--translation-secondary-color: #aaaaaa;
--translation-main-button-color: #272a2f;
--on-status-error-color: #ffffff;
--translation-subtitle-color: #888888;
--editor-background: #333941;
--editor-color: #e6e6e6;
Expand Down Expand Up @@ -108,6 +111,10 @@
--lilac: #c6c1f0;
--grey-5: #385465;

/* Insights and profile charts */
--chart-neutral: #272a2f;
--chart-label-muted: #4d5967;

/* Contribution graph (empty → most active) */
--contribution-graph-0: #333941;
--contribution-graph-1: #41554c;
Expand Down
81 changes: 54 additions & 27 deletions pontoon/base/static/css/light-theme.css
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
/* Light Theme Variables */
.light-theme {
/* Main */
--main-border-1: #d8d8d8;
--main-border-1: #c4c4c4;
/* Form controls */
--control-border: #8c8c8c;
--focus-ring: #1f86b8;
--moz-logo: url(../img/moz-logo.svg);

/* User banner and details */
--user-admin: #ff3366;
--user-pm: #ffa10f;
--user-manager: #4fc4f6;
--user-translator: #7bc876;
--user-new: #fed271;
--user-admin: var(--status-error);
--user-pm: var(--status-warning);
--user-manager: var(--status-unreviewed);
--user-translator: var(--status-translated);
--user-new: var(--status-fuzzy);

/* Primary (darker) background */
--background-1: #f6f6f6;
Expand All @@ -19,8 +22,8 @@
--input-background-1: #ffffff;
--input-color-1: #2a2a2a;
--input-color-2: #000000;
--toggle-color-1: #999999;
--toggle-color-2: #d0d0d0;
--toggle-color-1: #6a6a6a;
--toggle-color-2: #bcbcbc;
--icon-background-1: #d0d0d0;
--icon-border-1: #bbbbbb;

Expand All @@ -35,31 +38,34 @@
--tooltip-background: #000000dd;
--tooltip-color: #ffffff;
--tooltip-color-2: #aaaaaa;
--tooltip-border: #4d5967;
--tooltip-surface-background: #d8dbdf;
--tooltip-surface-color: #2b2b2b;
--tooltip-surface-color-2: #565656;
--tooltip-border: #bbc1c8;

/* Homepage */
--homepage-background-image: url(../img/background-light.svg);
--homepage-tour-button-background: #ffffff;
--homepage-tour-button-color: #000000;

/* Translation status */
--status-translated: #7bc876;
--status-translated-alt: #49a643;
--status-pretranslated: #c0ff00;
--status-pretranslated-alt: #80a900;
--status-warning: #ffa10f;
--status-error: #ff3366;
--status-missing: #bec7d1;
--status-missing-alt: #5f7285;
--status-unreviewed: #4fc4f6;
--status-fuzzy: #fed271;
/* Translation status colors shared by fills, text, and icons */
--status-translated: #2f7a3b;
--status-pretranslated: #5b7a00;
--status-warning: #995f08;
--status-error: #d8143a;
--status-missing: #5f7285;
--status-unreviewed: #14749b;
--status-fuzzy: #846619;

/* Translation workspace */
--translation-background: #f6f6f6;
--translation-border: #bbbbbb;
--translation-color: #2a2a2a;
--translation-secondary-color: #707070;
--translation-main-button-color: #2a2a2a;
--translation-secondary-color: #686868;
/* Main action buttons use status colors as backgrounds */
--translation-main-button-color: #ffffff;
/* Text on error-colored banners and badges */
--on-status-error-color: #ffffff;
--translation-subtitle-color: #555555;
--editor-background: #ffffff;
--editor-color: #2a2a2a;
Expand All @@ -77,14 +83,14 @@
--diff-del-color: #c61b3e;
--diff-ins-background: #e5feeb;
--diff-ins-color: #2c7022;
--search-color: #c98712;
--search-color: #8a5a00;
--search-background: #fff4e2;
--tags-brace-color: #6a1bcc;
--tags-bracket-color: #2c7560;
--tags-keyword-color: #6a1bcc;
--tags-literal-color: #2c7560;
--tags-name-color: #6a1bcc;
--tags-quote-color: #b66800;
--tags-quote-color: #9c5700;
--tags-tag-name-color: #2c7560;
--tags-string-color: #2a2a2a;

Expand All @@ -108,6 +114,10 @@
--lilac: #c6c1f0;
--grey-5: #385465;

/* Insights and profile charts */
--chart-neutral: #5f6772;
--chart-label-muted: #767676;

/* Contribution graph (empty → most active) */
--contribution-graph-0: #ebedf0;
--contribution-graph-1: #aadfa3;
Expand All @@ -127,13 +137,30 @@
--purple: #8074a8;
--green-brown: #7c7270;

--white-1: #2a2a2a;
--white-1: #3a3a3a;
--light-grey-6: #555555;
--light-grey-7: #707070;
--light-grey-2: #777777;
--light-grey-7: #5c5c5c;
--light-grey-2: #6e6e6e;
--light-grey-1: #bbbbbb;
--grey-3: #e8e8e8;
--dark-grey-2: #e8e8e8;
--dark-grey-1: #ffffff;
--black-3: #f6f6f6;
}

/* Separate light tooltips from the page */
.light-theme .table .latest-activity .tooltip {
border: 1px solid var(--tooltip-border);
box-shadow: 0 4px 12px -4px #00000040;
}

/* Show a high-contrast outline for keyboard-style focus in both themes.
Lives here because the translate bundle only loads the theme stylesheets. */
:is(a, button, input, textarea, select, [tabindex]):focus-visible {
outline: 2px solid var(--focus-ring);
outline-offset: 1px;
}

.light-theme .failed-checks .warning:before {
color: var(--status-warning);
}
4 changes: 2 additions & 2 deletions pontoon/base/static/css/multiple_item_selector.css
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
margin-bottom: 0;

li span.code {
color: var(--status-translated-alt);
color: var(--status-translated);
float: right;
text-align: left;
width: auto;
Expand All @@ -66,7 +66,7 @@

a:link,
a:visited {
color: var(--status-translated-alt);
color: var(--status-translated);
float: right;
text-transform: uppercase;
}
Expand Down
Loading