', {
+ class: 'no-results',
+ html: 'Oops, something went wrong.',
+ });
+
+ panel.append(message);
+ }
+ },
+ });
+ }
+
+ // Load panel content on page load
+ loadPanelContent(window.location.pathname);
+
+ // Load panel content on history change
+ window.onpopstate = function () {
+ loadPanelContent(window.location.pathname);
+ };
+
+ // Load panel content on menu click
+ container.on(
+ 'click',
+ '.left-column a, .right-column .use-as-template',
+ function (e) {
+ // Keep default middle-, shift-, control- and command-click behaviour
+ if (e.which === 2 || e.metaKey || e.shiftKey || e.ctrlKey) {
+ return;
+ }
+
+ e.preventDefault();
+
+ const path = $(this).attr('href');
+ loadPanelContent(path);
+ window.history.pushState({}, '', path);
+ },
+ );
+
// Toggle check box
- $('.check-box').click(function () {
+ container.on('click', '.check-box', function () {
const self = $(this);
+ const checkbox = self.find('[type=checkbox]')[0];
- const name = self.data('attribute');
- $(`[type=checkbox][name=${name}]`).click();
-
+ checkbox.checked = !checkbox.checked;
self.toggleClass('enabled');
// Toggle Transactional check box
@@ -182,14 +258,6 @@ $(function () {
}
});
- // Make sure custom checkboxes reflect the state of the HTML checkboxes
- // TODO: Replace checkboxes with native HTML checkboxes and style them with CSS
- $(`[type=checkbox]`).each(function () {
- const name = $(this).attr('name');
- const isChecked = $(this)[0].checked;
- $(`.check-box[data-attribute=${name}]`).toggleClass('enabled', isChecked);
- });
-
// Toggle between Edit and Review mode
container.on('click', '.controls .toggle.button', function (e) {
e.preventDefault();
@@ -227,10 +295,11 @@ $(function () {
container.on('click', '.controls .send.button', function (e) {
e.preventDefault();
- // Distinguish between Send and Send to myself
- $('.send-to-myself').prop('checked', $(this).is('.to-myself'));
-
const $form = $('#send-message');
+ const sendToMyself = $(this).is('.to-myself');
+
+ // Distinguish between Send and Send to myself
+ $('#id_send_to_myself').prop('checked', sendToMyself);
// Submit form
$.ajax({
@@ -239,6 +308,9 @@ $(function () {
data: $form.serialize(),
success: function () {
Pontoon.endLoader('Message sent.');
+ if (!sendToMyself) {
+ container.find('.left-column .sent a').click();
+ }
},
error: function () {
Pontoon.endLoader('Oops, something went wrong.', 'error');
diff --git a/pontoon/messaging/templates/messaging/includes/compose.html b/pontoon/messaging/templates/messaging/includes/compose.html
new file mode 100644
index 0000000000..098abb9cf3
--- /dev/null
+++ b/pontoon/messaging/templates/messaging/includes/compose.html
@@ -0,0 +1,224 @@
+{% import "widgets/checkbox.html" as Checkbox %}
+{% import 'teams/widgets/multiple_team_selector.html' as multiple_team_selector %}
+{% import 'widgets/multiple_item_selector.html' as multiple_item_selector %}
+
+
+
+
+
+
+
+
Review message
+
+
+
+
+
+
+
+
+
+
+
Recipients
+
+
User roles
+
+
+
+
Locales
+
+
+
+
Projects
+
+
+
+
Submitted translations
+
+
+
+
+
Performed reviews
+
+
+
+
+
Last login
+
+
+
+
+
Message type
+
+
Warning: Transactional emails are also sent to users who have
+ not opted in to email communication. They are restricted in the type of content that can be
+ included. When in doubt, please review with legal.
+
+
+
diff --git a/pontoon/messaging/templates/messaging/includes/sent.html b/pontoon/messaging/templates/messaging/includes/sent.html
new file mode 100644
index 0000000000..fa9bb50b75
--- /dev/null
+++ b/pontoon/messaging/templates/messaging/includes/sent.html
@@ -0,0 +1,39 @@
+
Warning: Transactional emails are also sent to users who have
- not opted in to email communication. They are restricted in the type of content that can be
- included. When in doubt, please review with legal.
-
-
-
-
-
-
-
diff --git a/pontoon/messaging/urls.py b/pontoon/messaging/urls.py
index 434b55d6df..a5a5ce3eb6 100644
--- a/pontoon/messaging/urls.py
+++ b/pontoon/messaging/urls.py
@@ -1,4 +1,4 @@
-from django.urls import path
+from django.urls import include, path
from . import views
@@ -7,13 +7,60 @@
# Messaging center
path(
"messaging/",
- views.messaging,
- name="pontoon.messaging",
- ),
- path(
- "send-message/",
- views.send_message,
- name="pontoon.messaging.send_message",
+ include(
+ [
+ # Compose
+ path(
+ "",
+ views.messaging,
+ name="pontoon.messaging.compose",
+ ),
+ # Edit as new
+ path(
+ "/",
+ views.messaging,
+ name="pontoon.messaging.use_as_template",
+ ),
+ # Sent
+ path(
+ "sent/",
+ views.messaging,
+ name="pontoon.messaging.sent",
+ ),
+ # AJAX views
+ path(
+ "ajax/",
+ include(
+ [
+ # Compose
+ path(
+ "",
+ views.ajax_compose,
+ name="pontoon.messaging.ajax.compose",
+ ),
+ # Edit as new
+ path(
+ "/",
+ views.ajax_use_as_template,
+ name="pontoon.messaging.ajax.use_as_template",
+ ),
+ # Sent
+ path(
+ "sent/",
+ views.ajax_sent,
+ name="pontoon.messaging.ajax.sent",
+ ),
+ # Send message
+ path(
+ "send/",
+ views.send_message,
+ name="pontoon.messaging.ajax.send_message",
+ ),
+ ]
+ ),
+ ),
+ ]
+ ),
),
# Email consent
path(
diff --git a/pontoon/messaging/views.py b/pontoon/messaging/views.py
index 5180f72ada..b6b661bd97 100644
--- a/pontoon/messaging/views.py
+++ b/pontoon/messaging/views.py
@@ -20,12 +20,13 @@
from pontoon.base.models import Locale, Project, Translation, UserProfile
from pontoon.base.utils import require_AJAX, split_ints
from pontoon.messaging import forms, utils
+from pontoon.messaging.models import Message
log = logging.getLogger(__name__)
-def messaging(request):
+def messaging(request, pk=None):
if not request.user.has_perm("base.can_manage_project"):
raise PermissionDenied
@@ -33,8 +34,63 @@ def messaging(request):
request,
"messaging/messaging.html",
{
- "available_locales": Locale.objects.available(),
- "available_projects": Project.objects.available().order_by("name"),
+ "count": Message.objects.count(),
+ },
+ )
+
+
+@require_AJAX
+def ajax_compose(request):
+ if not request.user.has_perm("base.can_manage_project"):
+ raise PermissionDenied
+
+ return render(
+ request,
+ "messaging/includes/compose.html",
+ {
+ "form": forms.MessageForm(),
+ "available_locales": [],
+ "selected_locales": Locale.objects.available(),
+ "available_projects": [],
+ "selected_projects": Project.objects.available().order_by("name"),
+ },
+ )
+
+
+@require_AJAX
+def ajax_use_as_template(request, pk):
+ if not request.user.has_perm("base.can_manage_project"):
+ raise PermissionDenied
+
+ message = get_object_or_404(Message, pk=pk)
+
+ return render(
+ request,
+ "messaging/includes/compose.html",
+ {
+ "form": forms.MessageForm(instance=message),
+ "available_locales": Locale.objects.available().exclude(
+ pk__in=message.locales.all()
+ ),
+ "selected_locales": message.locales.all(),
+ "available_projects": Project.objects.available().exclude(
+ pk__in=message.projects.all()
+ ),
+ "selected_projects": message.projects.all().order_by("name"),
+ },
+ )
+
+
+@require_AJAX
+def ajax_sent(request):
+ if not request.user.has_perm("base.can_manage_project"):
+ raise PermissionDenied
+
+ return render(
+ request,
+ "messaging/includes/sent.html",
+ {
+ "sent_messages": Message.objects.order_by("-sent_at"),
},
)
@@ -49,7 +105,7 @@ def get_recipients(form):
- Translators of selected Locales
"""
locale_ids = sorted(split_ints(form.cleaned_data.get("locales")))
- project_ids = sorted(split_ints(form.cleaned_data.get("projects")))
+ project_ids = form.cleaned_data.get("projects")
translations = Translation.objects.filter(
locale_id__in=locale_ids,
entity__resource__project_id__in=project_ids,
@@ -168,21 +224,22 @@ def send_message(request):
form = forms.MessageForm(request.POST)
if not form.is_valid():
- return JsonResponse(dict(form.errors.items()))
+ return JsonResponse(dict(form.errors.items()), status=400)
+
+ send_to_myself = form.cleaned_data.get("send_to_myself")
+ recipients = User.objects.filter(pk=request.user.pk)
- if form.cleaned_data.get("send_to_myself"):
- recipients = User.objects.filter(pk=request.user.pk)
- else:
+ """
+ While the feature is in development, messages are sent only to the current user.
+ TODO: Uncomment lines below when the feature is ready.
+ if not send_to_myself:
recipients = get_recipients(form)
+ """
log.info(
f"{recipients.count()} Recipients: {list(recipients.values_list('email', flat=True))}"
)
- # While the feature is in development, notifications and emails are sent only to the current user.
- # TODO: Remove this line when the feature is ready
- recipients = User.objects.filter(pk=request.user.pk)
-
is_notification = form.cleaned_data.get("notification")
is_email = form.cleaned_data.get("email")
is_transactional = form.cleaned_data.get("transactional")
@@ -236,6 +293,21 @@ def send_message(request):
f"Email sent to the following {email_recipients.count()} users: {email_recipients.values_list('email', flat=True)}."
)
+ if not send_to_myself:
+ message = form.save(commit=False)
+ message.sender = request.user
+ message.save()
+
+ message.recipients.set(recipients)
+
+ locale_ids = sorted(split_ints(form.cleaned_data.get("locales")))
+ locales = Locale.objects.filter(pk__in=locale_ids)
+ message.locales.set(locales)
+
+ project_ids = form.cleaned_data.get("projects")
+ projects = Project.objects.filter(pk__in=project_ids)
+ message.projects.set(projects)
+
return JsonResponse(
{
"status": True,
diff --git a/pontoon/settings/base.py b/pontoon/settings/base.py
index 327290db6a..8c294504ca 100644
--- a/pontoon/settings/base.py
+++ b/pontoon/settings/base.py
@@ -705,7 +705,6 @@ def _default_from_email():
"messaging": {
"source_filenames": (
"js/lib/showdown.js",
- "js/sidebar_menu.js",
"js/multiple_team_selector.js",
"js/multiple_item_selector.js",
"js/messaging.js",
diff --git a/pontoon/urls.py b/pontoon/urls.py
index 8968417d84..8cf369f242 100644
--- a/pontoon/urls.py
+++ b/pontoon/urls.py
@@ -55,13 +55,13 @@ class LocaleConverter(StringConverter):
# Include URL configurations from installed apps
path("terminology/", include("pontoon.terminology.urls")),
path("translations/", include("pontoon.translations.urls")),
+ path("", include("pontoon.messaging.urls")),
path("", include("pontoon.teams.urls")),
path("", include("pontoon.tour.urls")),
path("", include("pontoon.tags.urls")),
path("", include("pontoon.sync.urls")),
path("", include("pontoon.projects.urls")),
path("", include("pontoon.machinery.urls")),
- path("", include("pontoon.messaging.urls")),
path("", include("pontoon.insights.urls")),
path("", include("pontoon.contributors.urls")),
path("", include("pontoon.localizations.urls")),