зеркало из https://github.com/mozilla/pontoon.git
Add ability to send Monthly activity summary (#3474)
The changeset also factors out the base HTML email template, which is currently shared with the notifications email template. In the future, we'll use it for all HTML emails we send. In addition to the spec, a configurable Thank you note is added. The day of the month to send the summary emails on is also configurable.
This commit is contained in:
Родитель
d3d5cf111f
Коммит
eb65ceedae
|
@ -165,6 +165,10 @@ you create:
|
|||
Optional. Text to be shown in the footer of the non-transactional emails sent
|
||||
using the Messaging Center, just above the unsubscribe text.
|
||||
|
||||
``+EMAIL_MONTHLY_ACTIVITY_SUMMARY_INTRO``
|
||||
Optional. Custom text to be shown in the Monthly activity summary emails after the
|
||||
greeting and before the stats.
|
||||
|
||||
``ENABLE_BUGS_TAB``
|
||||
Optional. Enables Bugs tab on team pages, which pulls team data from
|
||||
bugzilla.mozilla.org. Specific for Mozilla deployments.
|
||||
|
@ -209,6 +213,11 @@ you create:
|
|||
Optional. Set your `Microsoft Translator API`_ key to use machine translation
|
||||
by Microsoft.
|
||||
|
||||
``MONTHLY_ACTIVITY_SUMMARY_DAY``
|
||||
Optional. Integer representing a day of the month on which the Monthly
|
||||
activity summary emails will be sent. 1 represents the first day of the month.
|
||||
The default value is 1.
|
||||
|
||||
``NEW_RELIC_API_KEY``
|
||||
Optional. API key for accessing the New Relic REST API. Used to mark deploys
|
||||
on New Relic.
|
||||
|
|
|
@ -1,23 +1,251 @@
|
|||
import calendar
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from notifications.models import Notification
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.db.models import Q
|
||||
from django.db.models import Count, F, Min, Prefetch, Q, Sum
|
||||
from django.template.loader import get_template
|
||||
from django.utils import timezone
|
||||
|
||||
from pontoon.actionlog.models import ActionLog
|
||||
from pontoon.base.models import Locale
|
||||
from pontoon.insights.models import LocaleInsightsSnapshot
|
||||
from pontoon.messaging.utils import html_to_plain_text_with_links
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_monthly_user_actions(users, months_ago):
|
||||
month_date = timezone.now() - relativedelta(months=months_ago)
|
||||
|
||||
actions = (
|
||||
ActionLog.objects.filter(
|
||||
performed_by__in=users,
|
||||
created_at__month=month_date.month,
|
||||
created_at__year=month_date.year,
|
||||
)
|
||||
.values("performed_by")
|
||||
.annotate(
|
||||
submitted=Count("id", filter=Q(action_type="translation:created")),
|
||||
reviewed=Count(
|
||||
"id",
|
||||
filter=Q(
|
||||
action_type__in=["translation:approved", "translation:rejected"]
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
return {action["performed_by"]: action for action in actions}
|
||||
|
||||
|
||||
def _get_monthly_locale_actions(months_ago):
|
||||
month_date = timezone.now() - relativedelta(months=months_ago)
|
||||
|
||||
snapshots = (
|
||||
LocaleInsightsSnapshot.objects.filter(
|
||||
created_at__month=month_date.month,
|
||||
created_at__year=month_date.year,
|
||||
)
|
||||
.values("locale")
|
||||
.annotate(
|
||||
added_source_strings=Sum("new_source_strings"),
|
||||
submitted=Sum(F("human_translations") + F("machinery_translations")),
|
||||
reviewed=Sum(F("peer_approved") + F("rejected")),
|
||||
)
|
||||
)
|
||||
|
||||
return {snapshot["locale"]: snapshot for snapshot in snapshots}
|
||||
|
||||
|
||||
def _get_monthly_locale_stats(months_ago):
|
||||
month_date = timezone.now() - relativedelta(months=months_ago)
|
||||
last_day = calendar.monthrange(month_date.year, month_date.month)[1]
|
||||
|
||||
snapshots = LocaleInsightsSnapshot.objects.filter(
|
||||
created_at__day=last_day,
|
||||
created_at__month=month_date.month,
|
||||
created_at__year=month_date.year,
|
||||
)
|
||||
|
||||
return {snapshot.locale_id: snapshot for snapshot in snapshots}
|
||||
|
||||
|
||||
def _get_monthly_locale_contributors(locales, months_ago):
|
||||
month_date = timezone.now() - relativedelta(months=months_ago)
|
||||
|
||||
actions = ActionLog.objects.filter(
|
||||
performed_by__profile__system_user=False,
|
||||
# Exclude system projects
|
||||
translation__entity__resource__project__system_project=False,
|
||||
)
|
||||
|
||||
# Get contributors that started contributing to the locale in the given month
|
||||
first_contributions = (
|
||||
actions.values("performed_by", "translation__locale")
|
||||
.annotate(first_contribution_date=Min("created_at"))
|
||||
.filter(
|
||||
first_contribution_date__month=month_date.month,
|
||||
first_contribution_date__year=month_date.year,
|
||||
)
|
||||
)
|
||||
|
||||
new_locale_contributors = {
|
||||
(entry["translation__locale"], entry["performed_by"])
|
||||
for entry in first_contributions
|
||||
}
|
||||
|
||||
# Get all contributors in the given month,
|
||||
# grouped by locale and orderd by contribution count
|
||||
monthly_contributors = (
|
||||
actions.filter(
|
||||
created_at__month=month_date.month,
|
||||
created_at__year=month_date.year,
|
||||
)
|
||||
.values("translation__locale", "performed_by")
|
||||
.annotate(contribution_count=Count("id"))
|
||||
.order_by("-contribution_count")
|
||||
)
|
||||
|
||||
# Group contributors by locale and user role
|
||||
results = {}
|
||||
all_locale_pks = [entry["translation__locale"] for entry in monthly_contributors]
|
||||
locales_dict = {locale.pk: locale for locale in locales}
|
||||
|
||||
all_user_pks = [entry["performed_by"] for entry in monthly_contributors]
|
||||
users = User.objects.filter(pk__in=all_user_pks)
|
||||
users_dict = {user.pk: user for user in users}
|
||||
|
||||
for locale_pk in all_locale_pks:
|
||||
locale_entries = [
|
||||
entry
|
||||
for entry in monthly_contributors
|
||||
if entry["translation__locale"] == locale_pk
|
||||
]
|
||||
locale = locales_dict.get(locale_pk)
|
||||
|
||||
new_contributors = []
|
||||
active_managers = []
|
||||
active_translators = []
|
||||
active_contributors = []
|
||||
|
||||
for entry in locale_entries:
|
||||
user_pk = entry["performed_by"]
|
||||
user = users_dict.get(user_pk)
|
||||
|
||||
if (locale_pk, user_pk) in new_locale_contributors:
|
||||
# Exclude staff users from new contributors
|
||||
if not user.is_staff:
|
||||
new_contributors.append(user)
|
||||
|
||||
if locale.managers_group.fetched_managers:
|
||||
active_managers.append(user)
|
||||
elif locale.translators_group.fetched_translators:
|
||||
active_translators.append(user)
|
||||
else:
|
||||
active_contributors.append(user)
|
||||
|
||||
results[locale_pk] = {
|
||||
"new_contributors": new_contributors,
|
||||
"active_managers": active_managers,
|
||||
"active_translators": active_translators,
|
||||
"active_contributors": active_contributors,
|
||||
}
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def send_monthly_activity_summary():
|
||||
"""
|
||||
Sends Monthly activity summary emails.
|
||||
"""
|
||||
log.info("Start sending Monthly activity summary emails.")
|
||||
|
||||
# Get user monthly actions
|
||||
users = User.objects.filter(profile__monthly_activity_summary=True)
|
||||
user_month_actions = _get_monthly_user_actions(users, months_ago=1)
|
||||
previous_user_month_actions = _get_monthly_user_actions(users, months_ago=2)
|
||||
|
||||
for user in users:
|
||||
user.month_actions = user_month_actions.get(user.pk, {})
|
||||
user.previous_month_actions = previous_user_month_actions.get(user.pk, {})
|
||||
|
||||
# Get locale monthly actions
|
||||
locales = Locale.objects.prefetch_related(
|
||||
Prefetch("managers_group__user_set", to_attr="fetched_managers"),
|
||||
Prefetch("translators_group__user_set", to_attr="fetched_translators"),
|
||||
)
|
||||
|
||||
locale_month_actions = _get_monthly_locale_actions(months_ago=1)
|
||||
locale_previous_month_actions = _get_monthly_locale_actions(months_ago=2)
|
||||
|
||||
locale_month_stats = _get_monthly_locale_stats(months_ago=1)
|
||||
locale_previous_month_stats = _get_monthly_locale_stats(months_ago=2)
|
||||
|
||||
locale_contributors = _get_monthly_locale_contributors(locales, months_ago=1)
|
||||
|
||||
for locale in locales:
|
||||
locale.month_actions = locale_month_actions.get(locale.pk, {})
|
||||
locale.previous_month_actions = locale_previous_month_actions.get(locale.pk, {})
|
||||
locale.month_stats = locale_month_stats.get(locale.pk, {})
|
||||
locale.previous_month_stats = locale_previous_month_stats.get(locale.pk, {})
|
||||
locale.contributors = locale_contributors.get(locale.pk, {})
|
||||
|
||||
# Create a map of users to locales in which they are managers or translators,
|
||||
# which determines if the user should receive the Team activity section of the email
|
||||
user_locales = defaultdict(set)
|
||||
for locale in locales:
|
||||
for user in locale.managers_group.fetched_managers:
|
||||
user_locales[user].add(locale)
|
||||
for user in locale.translators_group.fetched_translators:
|
||||
user_locales[user].add(locale)
|
||||
|
||||
# Process and send email for each user
|
||||
subject = "Monthly activity summary"
|
||||
template = get_template("messaging/emails/monthly_activity_summary.html")
|
||||
|
||||
now = timezone.now()
|
||||
current_month = now.month
|
||||
report_month = calendar.month_name[(current_month - 1) or 12]
|
||||
|
||||
current_year = now.year
|
||||
report_year = current_year if current_month > 1 else current_year - 1
|
||||
|
||||
for user in users:
|
||||
body_html = template.render(
|
||||
{
|
||||
"subject": subject,
|
||||
"month": report_month,
|
||||
"year": report_year,
|
||||
"user": user,
|
||||
"locales": user_locales.get(user, []),
|
||||
"settings": settings,
|
||||
}
|
||||
)
|
||||
body_text = html_to_plain_text_with_links(body_html)
|
||||
|
||||
msg = EmailMultiAlternatives(
|
||||
subject=subject,
|
||||
body=body_text,
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
to=[user.contact_email],
|
||||
)
|
||||
msg.attach_alternative(body_html, "text/html")
|
||||
msg.send()
|
||||
|
||||
recipient_count = len(users)
|
||||
|
||||
log.info(f"Monthly activity summary emails sent to {recipient_count} users.")
|
||||
|
||||
|
||||
def send_notification_digest(frequency="Daily"):
|
||||
"""
|
||||
Sends notification email digests to users based on the specified frequency (Daily or Weekly).
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils.timezone import now
|
||||
|
||||
from pontoon.messaging.emails import send_monthly_activity_summary
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Send monthly activity summary emails."
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"--force",
|
||||
action="store_true",
|
||||
help="Force sending regardless of the current date.",
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
# Only send on the given day of the month or when --force is used
|
||||
if options["force"] or now().day == settings.MONTHLY_ACTIVITY_SUMMARY_DAY:
|
||||
send_monthly_activity_summary()
|
||||
else:
|
||||
self.stdout.write(
|
||||
f"This command can only be run on day {settings.MONTHLY_ACTIVITY_SUMMARY_DAY} of the month. Use --force to bypass."
|
||||
)
|
|
@ -0,0 +1,111 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Content-Type" content="text/html charset=UTF-8" />
|
||||
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;600&display=swap" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
table {
|
||||
border-spacing: 0;
|
||||
color: #444444;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-weight: 300;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
table.inner {
|
||||
max-width: 600px;
|
||||
padding: 70px 0;
|
||||
}
|
||||
|
||||
table.inner a {
|
||||
color: #F36;
|
||||
}
|
||||
|
||||
.logo {
|
||||
padding-bottom: 30px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.header {
|
||||
font-size: 36px;
|
||||
margin: 20px 0;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.subheader {
|
||||
font-size: 15px;
|
||||
margin: 10px 0 20px 0;
|
||||
padding-bottom: 30px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
font-size: 12px;
|
||||
color: #888888;
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
{% block extend_css %}{% endblock %}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<table class="outer">
|
||||
<tr>
|
||||
<td>
|
||||
<table class="inner">
|
||||
<tr>
|
||||
<td class="logo">
|
||||
<a href="{{ full_url('pontoon.homepage') }}">
|
||||
<img src="{{ full_static('img/logo.png') }}" alt="Pontoon logo" width="52" height="64">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="header">
|
||||
{% block header %}{% endblock %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="subheader">
|
||||
{% block subheader %}{% endblock %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="content {% block content_class %}{% endblock %}">
|
||||
{% block content %}{% endblock %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="footer">
|
||||
{% block footer %}{% endblock %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,130 @@
|
|||
{% extends 'messaging/emails/base.html' %}
|
||||
|
||||
{% block title %}{{ subject }}{% endblock %}
|
||||
|
||||
{% block extend_css %}
|
||||
.subheader p:not(:last-child) {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 18px;
|
||||
font-weight: 100;
|
||||
margin: 0;
|
||||
padding-bottom: 10px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h2.team-activity {
|
||||
padding-top: 30px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 15px;
|
||||
line-height: 1.6em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
p.completion {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.bullet {
|
||||
display: inline-block;
|
||||
border: 1px solid;
|
||||
border-radius: 50%;
|
||||
margin: 1px 4px;
|
||||
height: 7px;
|
||||
width: 7px;
|
||||
}
|
||||
|
||||
.bullet.missing {
|
||||
background: #bec7d1;
|
||||
border-color: #bec7d1;
|
||||
}
|
||||
|
||||
.bullet.unreviewed {
|
||||
background: #4fc4f6;
|
||||
border-color: #4fc4f6;
|
||||
}
|
||||
|
||||
.bullet.pretranslated {
|
||||
background: #c0ff00;
|
||||
border-color: #c0ff00;
|
||||
}
|
||||
{% endblock %}
|
||||
|
||||
{% block header %}Hello.{% endblock %}
|
||||
|
||||
{% block subheader %}
|
||||
<p>Here's your monthly activity summary for {{ month }} {{ year }}.</p>
|
||||
|
||||
{% if settings.EMAIL_MONTHLY_ACTIVITY_SUMMARY_INTRO %}
|
||||
<p>{{ settings.EMAIL_MONTHLY_ACTIVITY_SUMMARY_INTRO }}</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content_class %}monthly-activity-summary{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>You personal activity</h2>
|
||||
{{ actions_item("Translations submitted", user, "submitted") }}
|
||||
{{ actions_item("Suggestions reviewed", user, "reviewed") }}
|
||||
|
||||
{% for locale in locales %}
|
||||
<h2 class="team-activity">Team activity for {{ locale.name }} ({{ locale.code }})</h2>
|
||||
|
||||
{{ actions_item("New source strings added", locale, "added_source_strings") }}
|
||||
{{ actions_item("Translations submitted", locale, "submitted") }}
|
||||
{{ actions_item("Suggestions reviewed", locale, "reviewed") }}
|
||||
|
||||
{{ contributors_item("New contributors", locale.contributors.new_contributors) }}
|
||||
{{ contributors_item("Recently active managers", locale.contributors.active_managers) }}
|
||||
{{ contributors_item("Recently active translators", locale.contributors.active_translators) }}
|
||||
{{ contributors_item("Recently active contributors", locale.contributors.active_contributors) }}
|
||||
|
||||
{{ status_item("Completion", locale, "completion", "completion", "", "%") }}
|
||||
{{ status_item("Strings with missing translations", locale, "missing_strings", "", "missing") }}
|
||||
{{ status_item("Unreviewed suggestions", locale, "unreviewed_strings", "", "unreviewed") }}
|
||||
{{ status_item("Unreviewed pretranslations", locale, "pretranslated_strings", "", "pretranslated") }}
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
{% block footer %}
|
||||
If you no longer want to receive these activity summaries, you can turn off the “Monthly activity summary“ feature in
|
||||
your <a href="{{ full_url('pontoon.contributors.settings') }}">Settings</a>.
|
||||
{% endblock %}
|
||||
|
||||
{% macro actions_item(label, actions_object, attribute) %}
|
||||
<p>
|
||||
<span class="bullet"></span>
|
||||
{{ label }}:
|
||||
{{ actions_object.month_actions[attribute] or 0 }} (vs. {{ actions_object.previous_month_actions[attribute] or 0 }} in the previous month)
|
||||
</p>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro contributors_item(label, contributors) %}
|
||||
{% if contributors %}
|
||||
<p>
|
||||
<span class="bullet"></span>
|
||||
{{ label }}:
|
||||
{% for contributor in contributors %}
|
||||
<a href="{{ full_url('pontoon.contributors.contributor.username', contributor.username) }}">
|
||||
<span class="name">{{ contributor.name_or_email }}</span></a>{% if not loop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro status_item(label, locale, attribute, cls="", url_parameter="", unit="") %}
|
||||
<p class="{{ cls }}">
|
||||
<span class="bullet {{ url_parameter }}"></span>
|
||||
{{ label }}:
|
||||
{% if url_parameter %}
|
||||
<a href="{{ full_url('pontoon.translate', locale.code, 'all-projects', 'all-resources') }}?status={{ url_parameter }}">{{ locale.month_stats[attribute] or 0 }}{{ unit }}</a>
|
||||
{% else %}
|
||||
{{ locale.month_stats[attribute] or 0 }}{{ unit }}
|
||||
{% endif %}
|
||||
(vs. {{ locale.previous_month_stats[attribute] or 0 }}{{ unit }} in the previous month)
|
||||
</p>
|
||||
{% endmacro %}
|
|
@ -1,165 +1,75 @@
|
|||
{% extends 'messaging/emails/base.html' %}
|
||||
{% import "contributors/widgets/notifications_menu.html" as Notifications with context %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
{% block title %}{{ subject }}{% endblock %}
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Content-Type" content="text/html charset=UTF-8" />
|
||||
{% block extend_css %}
|
||||
.notifications {
|
||||
font-size: 15px;
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
<title>{{ subject }}</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;600&display=swap" rel="stylesheet">
|
||||
.notifications ul {
|
||||
border-bottom: 1px solid #ededed;
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
<style>
|
||||
/* General Styles */
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.notifications li {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
table {
|
||||
border-spacing: 0;
|
||||
color: #444444;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-weight: 300;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
.notifications li.horizontal-separator {
|
||||
border-top: 1px solid #ededed;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
.notifications li .item-content {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100px;
|
||||
height: auto;
|
||||
}
|
||||
.notifications li .item-content .message {
|
||||
color: #888888;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
table.inner {
|
||||
max-width: 600px;
|
||||
padding: 70px 0;
|
||||
}
|
||||
.notifications li .item-content .message p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
table.inner a {
|
||||
color: #F36;
|
||||
}
|
||||
.notifications li .item-content .description ul {
|
||||
border: none;
|
||||
list-style: inside;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
padding-bottom: 30px;
|
||||
text-align: right;
|
||||
}
|
||||
.notifications li .item-content .description ul li {
|
||||
margin: 0;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 36px;
|
||||
margin: 20px 0;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.notifications li .item-content .timeago {
|
||||
color: #888888;
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
font-weight: normal;
|
||||
margin-top: 8px;
|
||||
text-align: right;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
{% endblock %}
|
||||
|
||||
.subtitle {
|
||||
font-size: 15px;
|
||||
margin: 10px 0 20px 0;
|
||||
padding-bottom: 30px;
|
||||
}
|
||||
{% block header %}Hello.{% endblock %}
|
||||
|
||||
.footer {
|
||||
font-size: 12px;
|
||||
color: #888888;
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
}
|
||||
{% block subheader %}Here's a summary of the Pontoon notifications you've subscribed to.{% endblock %}
|
||||
|
||||
/* Notifications */
|
||||
.notifications {
|
||||
font-size: 15px;
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
}
|
||||
{% block content_class %}notifications{% endblock %}
|
||||
|
||||
.notifications ul {
|
||||
border-bottom: 1px solid #ededed;
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
{% block content %}
|
||||
{{ Notifications.list(notifications=notifications) }}
|
||||
{% endblock %}
|
||||
|
||||
.notifications li {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.notifications li.horizontal-separator {
|
||||
border-top: 1px solid #ededed;
|
||||
}
|
||||
|
||||
.notifications li .item-content {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.notifications li .item-content .message {
|
||||
color: #888888;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.notifications li .item-content .message p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.notifications li .item-content .description ul {
|
||||
border: none;
|
||||
list-style: inside;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.notifications li .item-content .description ul li {
|
||||
margin: 0;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
.notifications li .item-content .timeago {
|
||||
color: #888888;
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
font-weight: normal;
|
||||
margin-top: 8px;
|
||||
text-align: right;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<table class="outer">
|
||||
<tr>
|
||||
<td>
|
||||
<table class="inner">
|
||||
<tr>
|
||||
<td class="logo">
|
||||
<a href="{{ full_url('pontoon.homepage') }}">
|
||||
<img src="{{ full_static('img/logo.png') }}" alt="Pontoon logo" width="52" height="64">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">Hello.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="subtitle">Here's a summary of the Pontoon notifications you've subscribed to.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="notifications">
|
||||
{{ Notifications.list(notifications=notifications) }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="footer">
|
||||
You can adjust your notifications subscriptions at any time from your
|
||||
<a href="{{ full_url('pontoon.contributors.settings') }}">Settings</a>.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
{% block footer %}
|
||||
You can adjust your notifications subscriptions at any time from your <a href="{{ full_url('pontoon.contributors.settings') }}">Settings</a>.
|
||||
{% endblock %}
|
||||
|
|
|
@ -226,6 +226,9 @@ EMAIL_COMMUNICATIONS_HELP_TEXT = os.environ.get("EMAIL_COMMUNICATIONS_HELP_TEXT"
|
|||
EMAIL_COMMUNICATIONS_FOOTER_PRE_TEXT = os.environ.get(
|
||||
"EMAIL_COMMUNICATIONS_FOOTER_PRE_TEXT", ""
|
||||
)
|
||||
EMAIL_MONTHLY_ACTIVITY_SUMMARY_INTRO = os.environ.get(
|
||||
"EMAIL_MONTHLY_ACTIVITY_SUMMARY_INTRO", ""
|
||||
)
|
||||
|
||||
# Log emails to console if the SendGrid credentials are missing.
|
||||
if EMAIL_HOST_USER and EMAIL_HOST_PASSWORD:
|
||||
|
@ -1124,6 +1127,10 @@ SUGGESTION_NOTIFICATIONS_DAY = os.environ.get("SUGGESTION_NOTIFICATIONS_DAY", 4)
|
|||
# is 4 (Friday).
|
||||
NOTIFICATION_DIGEST_DAY = os.environ.get("NOTIFICATION_DIGEST_DAY", 4)
|
||||
|
||||
# Integer representing a day of the month on which the Monthly activity summary
|
||||
# email will be sent.
|
||||
MONTHLY_ACTIVITY_SUMMARY_DAY = os.environ.get("MONTHLY_ACTIVITY_SUMMARY_DAY", 1)
|
||||
|
||||
# Date from which badge data collection starts
|
||||
badges_start_date = os.environ.get("BADGES_START_DATE", "1970-01-01")
|
||||
try:
|
||||
|
|
Загрузка…
Ссылка в новой задаче