Repository: mozilla/pontoon

Issue:
[notifications] Notify project Translators when suggestions are submitted
*This issue was created automatically by a [script](https://github.com/mathjazz/bugzilla2github/).*
## [Bug 1398861](https://bugzilla.mozilla.org/show_bug.cgi?id=1398861)
Bug Reporter: @mathjazz
CC:   @MikkCZ, mvasicek@mozilla.cz
See also:     https://bugzilla.mozilla.org/show_bug.cgi?id=1625102

We should probably do that periodically and aggregate notifications.


Discussion:
Comment Author: @mathjazz

*** Bug #1403303 has been marked as a duplicate of this bug. ***


Comment Author: @mathjazz

We should also notify comment authors.


Comment Author: @mathjazz

*** Bug #1625102 has been marked as a duplicate of this bug. ***



Resolving patch:
diff --git a/pontoon/projects/management/commands/send_suggestion_notifications.py b/pontoon/projects/management/commands/send_suggestion_notifications.py
new file mode 100644
--- /dev/null
+++ b/pontoon/projects/management/commands/send_suggestion_notifications.py
@@ -0,0 +1,117 @@
+import calendar
+
+from collections import defaultdict
+from datetime import timedelta
+from functools import cached_property
+
+from django.conf import settings
+from django.contrib.auth.models import User
+from django.core.management.base import BaseCommand, CommandError
+from django.db.models import Prefetch, Q
+from django.template.loader import render_to_string
+from django.utils import timezone
+from notifications.signals import notify
+
+from pontoon.base.models import Comment, Locale, ProjectLocale, Translation
+
+
+class Command(BaseCommand):
+    help = "Notify contributors about newly added unreviewed suggestions"
+
+    def add_arguments(self, parser):
+        parser.add_argument(
+            "--force",
+            action="store_true",
+            dest="force",
+            default=False,
+            help="Force run command, regardless of what day of the week it is",
+        )
+
+    @cached_property
+    def locale_reviewers(self):
+        locales = Locale.objects.prefetch_related(
+            Prefetch("managers_group__user_set", to_attr="fetched_managers"),
+            Prefetch("translators_group__user_set", to_attr="fetched_translators"),
+        )
+
+        locale_reviewers = {}
+
+        for locale in locales:
+            managers = locale.managers_group.fetched_managers
+            translators = locale.translators_group.fetched_translators
+            locale_reviewers[locale] = managers + translators
+
+        return locale_reviewers
+
+    def extract_notifications_data(self, data, suggestion):
+        locale = suggestion.locale
+        entity = suggestion.entity
+        project = entity.resource.project
+        project_locale = ProjectLocale.objects.get(project=project, locale=locale)
+
+        translations = Translation.objects.filter(entity=entity, locale=locale)
+        recipients = set()
+
+        # Users with permission to review suggestions
+        recipients = recipients.union(self.locale_reviewers[locale])
+
+        # Authors of previous translations of the same string
+        recipients = recipients.union(User.objects.filter(translation__in=translations))
+
+        # Authors of comments of previous translations
+        translations_comments = Comment.objects.filter(translation__in=translations)
+        recipients = recipients.union(
+            User.objects.filter(comment__in=translations_comments)
+        )
+
+        # Authors of team comments of the same string
+        team_comments = Comment.objects.filter(entity=entity, locale=locale)
+        recipients = recipients.union(User.objects.filter(comment__in=team_comments))
+
+        for recipient in recipients:
+            data[recipient].add(project_locale)
+
+    def get_suggestions(self):
+        start = timezone.now() - timedelta(days=7)
+
+        return Translation.objects.filter(
+            approved=False, rejected=False, fuzzy=False
+        ).filter(
+            Q(date__gt=start)
+            | Q(unapproved_date__gt=start)
+            | Q(unrejected_date__gt=start)
+        )
+
+    def handle(self, *args, **options):
+        """
+        This command sends notifications about newly created unreviewed suggestions that
+        were submitted, unapproved or unrejected in the last 7 days. Recipients of
+        notifications are users with permission to review them, as well as authors of
+        previous translations or comments of the same string.
+
+        The command is designed to run on a weekly basis.
+        """
+        self.stdout.write("Sending suggestion notifications.")
+
+        today = calendar.day_name[timezone.datetime.today().weekday()]
+        day = calendar.day_name[settings.SUGGESTION_NOTIFICATIONS_DAY]
+
+        if today != day and not options["force"]:
+            raise CommandError(f"Skipping. Command runs every {day}. Today is {today}.")
+
+        suggestions = self.get_suggestions()
+
+        data = defaultdict(set)
+
+        for suggestion in suggestions:
+            self.extract_notifications_data(data, suggestion)
+
+        for recipient, project_locales in data.items():
+            verb = render_to_string(
+                "projects/suggestion_notification.jinja",
+                {"project_locales": project_locales},
+            )
+
+            notify.send(recipient, recipient=recipient, verb=verb)
+
+        self.stdout.write("Suggestion notifications sent.")
diff --git a/pontoon/settings/base.py b/pontoon/settings/base.py
--- a/pontoon/settings/base.py
+++ b/pontoon/settings/base.py
@@ -848,6 +848,10 @@ def account_username(user):
 # Maximum number of read notifications to display in the notifications menu
 NOTIFICATIONS_MAX_COUNT = 7
 
+# Integer representing a day of the week on which the `send_suggestion_notifications`
+# management command will run.
+SUGGESTION_NOTIFICATIONS_DAY = os.environ.get("SUGGESTION_NOTIFICATIONS_DAY", 4)
+
 # Number of events displayed on the Contributor's timeline per page.
 CONTRIBUTORS_TIMELINE_EVENTS_PER_PAGE = 10
 


Test patch: