diff --git a/funnel/__init__.py b/funnel/__init__.py
index 3a6e8af40..b08e60b55 100644
--- a/funnel/__init__.py
+++ b/funnel/__init__.py
@@ -8,6 +8,7 @@
from email.utils import parseaddr
import geoip2.database
+import phonenumbers
from flask import Flask
from flask_babel import get_locale
from flask_executor import Executor
@@ -120,6 +121,10 @@
each_app.config['MAIL_DEFAULT_SENDER_ADDR'] = parseaddr(
app.config['MAIL_DEFAULT_SENDER']
)[1]
+ each_app.config['SITE_SUPPORT_PHONE_FORMATTED'] = phonenumbers.format_number(
+ phonenumbers.parse(each_app.config['SITE_SUPPORT_PHONE']),
+ phonenumbers.PhoneNumberFormat.INTERNATIONAL,
+ )
proxies.init_app(each_app)
manifest.init_app(each_app)
db.init_app(each_app)
diff --git a/funnel/models/notification_types.py b/funnel/models/notification_types.py
index 30d771098..e418d8fda 100644
--- a/funnel/models/notification_types.py
+++ b/funnel/models/notification_types.py
@@ -30,6 +30,7 @@
'RegistrationCancellationNotification',
'RegistrationConfirmationNotification',
'ProjectStartingNotification',
+ 'ProjectPublishedNotification',
'OrganizationAdminMembershipNotification',
'OrganizationAdminMembershipRevokedNotification',
]
@@ -154,6 +155,22 @@ class ProjectStartingNotification(
# This is a notification triggered without an actor
+class ProjectPublishedNotification(
+ DocumentHasProfile, Notification, type='project_published'
+):
+ """Notifications of a newly published project."""
+
+ category = notification_categories.participant
+ title = __("When a project is published")
+ description = __(
+ "Notifies all members of a profile when a new project is published"
+ )
+
+ document_model = Project
+ roles = ['project_crew', 'project_participant', 'account_participant']
+ exclude_actor = False # Send to everyone including the actor
+
+
# --- Comment notifications ------------------------------------------------------------
diff --git a/funnel/templates/email_account_reset.html.jinja2 b/funnel/templates/email_account_reset.html.jinja2
index 9660924fa..7ff99c3da 100644
--- a/funnel/templates/email_account_reset.html.jinja2
+++ b/funnel/templates/email_account_reset.html.jinja2
@@ -1,16 +1,32 @@
{% extends "notifications/layout_email.html.jinja2" %}
-{% block content -%}
-
-
{% trans %}Hello {{ fullname }},{% endtrans %}
-
- {% trans %}You – or someone claiming to be you – asked for your password to be reset. This OTP is valid for 15 minutes.{% endtrans %}
-
- {% trans %}OTP:{% endtrans %} {{ otp }}
-
- {% trans %}You can also use this link:{% endtrans %}
-
- {% trans %}Reset your password{% endtrans %}
-
- {% trans %}If you did not ask for this, you may safely ignore this email.{% endtrans %}
+{%- from "notifications/macros_email.html.jinja2" import cta_button -%}
+{% block content -%}
+
+
+ {{ otp }}
+
+ {% trans %}You – or someone claiming to be you – asked for your password to be reset. This OTP is valid for 15 minutes.{% endtrans %}
+ |
+
+
+
+
+ {% trans %}You can also use this link:{% endtrans %}
+ |
+
+ {# Button : BEGIN #}
+ {{ cta_button(url, gettext("Reset your password") )}}
+ {# Button : END #}
+
+ |
+
+
+ |
+
+
+
+
{%- endblock content %}
diff --git a/funnel/templates/email_account_verify.html.jinja2 b/funnel/templates/email_account_verify.html.jinja2
index c8b6d0203..6cb1d568a 100644
--- a/funnel/templates/email_account_verify.html.jinja2
+++ b/funnel/templates/email_account_verify.html.jinja2
@@ -1,10 +1,24 @@
{% extends "notifications/layout_email.html.jinja2" %}
-{% block content -%}
-
- {% trans %}Hello {{ fullname }},{% endtrans %}
-
- {% trans %}Confirm your email address{% endtrans %}
-
- {% trans %}If you did not ask for this, you may safely ignore this email.{% endtrans %}
+{%- from "notifications/macros_email.html.jinja2" import cta_button -%}
+{% block content -%}
+
+
+ {% trans %}Hello {{ fullname }}{% endtrans %}
+ |
+
+ {# Button : BEGIN #}
+ {{ cta_button(url, gettext("Confirm your email address") )}}
+ {# Button : END #}
+
+ |
+
+
+ |
+
+
+
+
{%- endblock %}
diff --git a/funnel/templates/email_login_otp.html.jinja2 b/funnel/templates/email_login_otp.html.jinja2
index 0b41b05c2..6560ee39d 100644
--- a/funnel/templates/email_login_otp.html.jinja2
+++ b/funnel/templates/email_login_otp.html.jinja2
@@ -1,15 +1,19 @@
{% extends "notifications/layout_email.html.jinja2" %}
{% block content -%}
- {%- if fullname %}
- {% trans %}Hello {{ fullname }},{% endtrans %}
- {%- else %}
- {% trans %}Hello!{% endtrans %}
- {%- endif %}
- {% trans %}This login OTP is valid for 15 minutes.{% endtrans %}
-
- {% trans %}OTP:{% endtrans %} {{ otp }}
-
- {% trans %}If you did not ask for this, you may safely ignore this email.{% endtrans %}
-
+
+
+ {{ otp }}
+
+ {% trans %}This login OTP is valid for 15 minutes.{% endtrans %}
+ |
+
+
+ |
+
+
+
+
+ |
+
{%- endblock %}
diff --git a/funnel/templates/email_sudo_otp.html.jinja2 b/funnel/templates/email_sudo_otp.html.jinja2
index e513c4bb3..5455db9c6 100644
--- a/funnel/templates/email_sudo_otp.html.jinja2
+++ b/funnel/templates/email_sudo_otp.html.jinja2
@@ -1,10 +1,10 @@
{% extends "notifications/layout_email.html.jinja2" %}
{% block content -%}
-
- {% trans %}Hello {{ fullname }},{% endtrans %}
-
- {% trans %}You are about to perform a critical action. This OTP serves as your confirmation to proceed and is valid for 15 minutes.{% endtrans %}
-
- {% trans %}OTP:{% endtrans %} {{ otp }}
-
+
+
+ {{ otp }}
+
+ {% trans %}You are about to perform a critical action. This OTP serves as your confirmation to proceed and is valid for 15 minutes.{% endtrans %}
+ |
+
{%- endblock %}
diff --git a/funnel/templates/notifications/comment_received_email.html.jinja2 b/funnel/templates/notifications/comment_received_email.html.jinja2
index e8b456ae0..2084f8cab 100644
--- a/funnel/templates/notifications/comment_received_email.html.jinja2
+++ b/funnel/templates/notifications/comment_received_email.html.jinja2
@@ -1,19 +1,30 @@
{%- extends "notifications/layout_email.html.jinja2" -%}
+{%- from "notifications/macros_email.html.jinja2" import cta_button -%}
{%- block content -%}
- {%- if view.notification.document_type == 'project' -%}
-
- {%- elif view.notification.document_type == 'proposal' -%}
-
- {%- elif view.notification.document_type == 'comment' -%}
- {% trans %}You wrote:{% endtrans %}
- {{ view.document.message }}
- {%- endif %}
+
+
+ {%- if view.notification.document_type == 'project' -%}
+
+ {%- elif view.notification.document_type == 'proposal' -%}
+
+ {%- elif view.notification.document_type == 'comment' -%}
+ {% trans %}You wrote:{% endtrans %}
+ {{ view.document.message }}
+ {%- endif %}
- {{ view.activity_html() }}
+ {{ view.activity_html() }}
+ |
+
+
+
+ {{ view.comment.message }}
+ |
+
+
- {{ view.comment.message }}
-
- {% trans %}View comment{% endtrans %}
+ {# Button : BEGIN #}
+ {{ cta_button(view.comment.url_for(_external=true, **view.tracking_tags()), gettext("View comment") )}}
+ {# Button : END #}
{%- endblock content -%}
diff --git a/funnel/templates/notifications/comment_report_received_email.html.jinja2 b/funnel/templates/notifications/comment_report_received_email.html.jinja2
index 43b131d43..cce0610c9 100644
--- a/funnel/templates/notifications/comment_report_received_email.html.jinja2
+++ b/funnel/templates/notifications/comment_report_received_email.html.jinja2
@@ -1,14 +1,16 @@
-{%- extends "notifications/layout_email.html.jinja2" %}
+{%- extends "notifications/layout_email.html.jinja2" -%}
+{%- from "notifications/macros_email.html.jinja2" import cta_button -%}
{%- block content -%}
-
- {%- trans %}A comment has been reported as spam{% endtrans -%}
-
+
+
+ {%- trans %}A comment has been reported as spam{% endtrans -%}
+ |
+
+
-
-
- {%- trans %}Review comment{% endtrans -%}
-
-
+ {# Button : BEGIN #}
+ {{ cta_button(url_for('siteadmin_review_comment', report=view.report.uuid_b58), gettext("Review comment") )}}
+ {# Button : END #}
{%- endblock content -%}
diff --git a/funnel/templates/notifications/layout_email.html.jinja2 b/funnel/templates/notifications/layout_email.html.jinja2
index 1319a4ee9..72267f11e 100644
--- a/funnel/templates/notifications/layout_email.html.jinja2
+++ b/funnel/templates/notifications/layout_email.html.jinja2
@@ -1,45 +1,534 @@
+{%- from "notifications/macros_email.html.jinja2" import hero_image -%}
-
-
+
+
+ {# utf-8 works for most cases #}
+
+
+
+ {# What it does: Makes background images in 72ppi Outlook render at correct size. #}
+
+
+ {# Outlook / @font-face : BEGIN #}
+
+ {# Desktop Outlook chokes on web font references and defaults to Times New Roman, so we force a safe fallback font. #}
+
+
+ {# Outlook / @font-face : END #}
+
{% block stylesheet -%}
-
+ {# CSS Reset : BEGIN #}
+
+ {# CSS Reset : END #}
+
+ {# Progressive Enhancements : BEGIN #}
+
+ {# Progressive Enhancements : END #}
{%- endblock stylesheet %}
-
-
+
+ {# Element styles : BEGIN #}
+
+
+
+
+ {#
+ The email background color (#f0f0f0) is defined in three places:
+ 1. body tag: for most email clients
+ 2. center tag: for Gmail and Inbox mobile apps and web versions of Gmail, GSuite, Inbox, Yahoo, AOL, Libero, Comcast, freenet, Mail.ru, Orange.fr
+ 3. mso conditional: For Windows 10 Mail
+ #}
+
{%- block jsonld %}{%- if jsonld %}
-
+
{%- endif %}{%- endblock jsonld %}
- {% block content %}{% endblock content %}
-
-
+
+
+
+
+ {#
+ Set the email width. Defined in two places:
+ 1. max-width for all clients except Desktop Windows Outlook, allowing the email to squish on narrow but never go wider than 600px.
+ 2. MSO tags for Desktop Windows Outlook enforce a 600px width.
+ #}
+
+
+
+ {# Email Header : BEGIN #}
+
+ {# Email Header : END #}
+
+ {# Email Header : BODY #}
+
+
+
+
+
+
+
+ |
+
+
+
+ |
+
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+ {#
+ Set the email width. Defined in two places:
+ 1. max-width for all clients except Desktop Windows Outlook, allowing the email to squish on narrow but never go wider than 600px.
+ 2. MSO tags for Desktop Windows Outlook enforce a 600px width.
+ #}
+
+
+
+
+ {# Email content : BEGIN #}
+
+ {% if view.hero_image %}
+ {# Hero image centered : BEGIN #}
+ {{ hero_image(view.hero_image) }}
+ {# Hero image centered : END #}
+
+ {# Email body : BEGIN #}
+ {%- if view.email_heading %}
+
+
+ {{ view.email_heading }}
+ |
+
+ {%- endif %}
+ {% endif %}
+ {% block content %}{% endblock content %}
+
+ {# Email body : END #}
+
+
+ {# Email content : END #}
+
+
+
+ |
+
+ |
+
+
+
+
+
+
+ |
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+
+ {# Email Footer : BEGIN #}
+ {% block footer %}
+
+
+
+ {% endblock footer %}
+ {# Email Footer : END #}
+
+
+
diff --git a/funnel/templates/notifications/macros_email.html.jinja2 b/funnel/templates/notifications/macros_email.html.jinja2
index 6c398fdee..1f496f182 100644
--- a/funnel/templates/notifications/macros_email.html.jinja2
+++ b/funnel/templates/notifications/macros_email.html.jinja2
@@ -1,14 +1,68 @@
{%- macro pinned_update(view, project) -%}
- {%- with update=project.pinned_update -%}
- {%- if update -%}
+ {%- with update=project.pinned_update -%}
+ {%- if update -%}
+
+ |
+
+
+ |
+
+
+
+
+ {%- trans number=update.number|numberformat -%}Update #{{ number }}{%- endtrans %} • {% trans age=update.published_at|age, editor=update.user.pickername -%}Posted by {{ editor }} {{ age }}{%- endtrans -%}
+
+
+ {{ update.body }}
+ |
+
+ {%- endif -%}
+ {%- endwith -%}
+{%- endmacro -%}
-
- {%- trans number=update.number|numberformat -%}Update #{{ number }}{%- endtrans %} • {% trans age=update.published_at|age, editor=update.user.pickername -%}Posted by {{ editor }} {{ age }}{%- endtrans -%}
-
-
+{%- macro hero_image(img_url, alt_text) -%}
+
+
+
+ |
+
+{%- endmacro -%}
- {{ update.body }}
+{% macro cta_button(btn_url, btn_text) %}
+
+
+
+ |
+
+{% endmacro %}
- {%- endif -%}
- {%- endwith -%}
-{%- endmacro -%}
+{% macro rsvp_footer(view, rsvp_linktext) %}
+ {%- if view %}
+
+ |
+
+
+
+
+ |
+
+ {%- endif %}
+{% endmacro %}
diff --git a/funnel/templates/notifications/organization_membership_granted_email.html.jinja2 b/funnel/templates/notifications/organization_membership_granted_email.html.jinja2
index 7a4dfbe7f..34c8f04bb 100644
--- a/funnel/templates/notifications/organization_membership_granted_email.html.jinja2
+++ b/funnel/templates/notifications/organization_membership_granted_email.html.jinja2
@@ -1,10 +1,16 @@
{%- extends "notifications/layout_email.html.jinja2" -%}
+{%- from "notifications/macros_email.html.jinja2" import cta_button -%}
{%- block content -%}
- {{ view.activity_html() }}
-
-
- {%- trans %}See all admins{% endtrans -%}
-
-
+
+
+
+ {{ view.activity_html() }}
+ |
+
+
+
+ {# Button : BEGIN #}
+ {{ cta_button(view.organization.profile.url_for('members', _external=true, **view.tracking_tags()), gettext("See all admins") )}}
+ {# Button : END #}
+
{%- endblock content -%}
diff --git a/funnel/templates/notifications/organization_membership_revoked_email.html.jinja2 b/funnel/templates/notifications/organization_membership_revoked_email.html.jinja2
index 7a4dfbe7f..34c8f04bb 100644
--- a/funnel/templates/notifications/organization_membership_revoked_email.html.jinja2
+++ b/funnel/templates/notifications/organization_membership_revoked_email.html.jinja2
@@ -1,10 +1,16 @@
{%- extends "notifications/layout_email.html.jinja2" -%}
+{%- from "notifications/macros_email.html.jinja2" import cta_button -%}
{%- block content -%}
- {{ view.activity_html() }}
-
-
- {%- trans %}See all admins{% endtrans -%}
-
-
+
+
+
+ {{ view.activity_html() }}
+ |
+
+
+
+ {# Button : BEGIN #}
+ {{ cta_button(view.organization.profile.url_for('members', _external=true, **view.tracking_tags()), gettext("See all admins") )}}
+ {# Button : END #}
+
{%- endblock content -%}
diff --git a/funnel/templates/notifications/project_crew_membership_granted_email.html.jinja2 b/funnel/templates/notifications/project_crew_membership_granted_email.html.jinja2
index f48dbe234..6925a19cc 100644
--- a/funnel/templates/notifications/project_crew_membership_granted_email.html.jinja2
+++ b/funnel/templates/notifications/project_crew_membership_granted_email.html.jinja2
@@ -1,10 +1,16 @@
{%- extends "notifications/layout_email.html.jinja2" -%}
+{%- from "notifications/macros_email.html.jinja2" import cta_button -%}
{%- block content -%}
- {{ view.activity_html() }}
-
-
- {%- trans %}See all crew members{% endtrans -%}
-
-
+
+
+
+ {{ view.activity_html() }}
+ |
+
+
+
+ {# Button : BEGIN #}
+ {{ cta_button(view.project.url_for('crew', _external=true, **view.tracking_tags()), gettext("See all crew members") )}}
+ {# Button : END #}
+
{%- endblock content -%}
diff --git a/funnel/templates/notifications/project_crew_membership_revoked_email.html.jinja2 b/funnel/templates/notifications/project_crew_membership_revoked_email.html.jinja2
index 364066eb7..a0c59b27d 100644
--- a/funnel/templates/notifications/project_crew_membership_revoked_email.html.jinja2
+++ b/funnel/templates/notifications/project_crew_membership_revoked_email.html.jinja2
@@ -1,10 +1,16 @@
{%- extends "notifications/layout_email.html.jinja2" -%}
+{%- from "notifications/macros_email.html.jinja2" import cta_button -%}
{%- block content -%}
- {{ view.activity_html() }}
-
-
- {%- trans %}See all admins{% endtrans -%}
-
-
+
+
+
+ {{ view.activity_html() }}
+ |
+
+
+
+ {# Button : BEGIN #}
+ {{ cta_button(view.project.profile.url_for('members', _external=true, **view.tracking_tags()), gettext("See all admins") )}}
+ {# Button : END #}
+
{%- endblock content -%}
diff --git a/funnel/templates/notifications/project_published_email.html.jinja2 b/funnel/templates/notifications/project_published_email.html.jinja2
new file mode 100644
index 000000000..d05919e9a
--- /dev/null
+++ b/funnel/templates/notifications/project_published_email.html.jinja2
@@ -0,0 +1,127 @@
+{%- extends "notifications/layout_email.html.jinja2" -%}
+{%- from "notifications/macros_email.html.jinja2" import cta_button, rsvp_footer, pinned_update -%}
+
+{% block stylesheet %}
+
+{% endblock stylesheet %}
+{%- block content -%}
+
+
+
+ {%- if view.project.bg_image.url %}
+
+ {%- else %}
+
+ {% endif %}
+ |
+
+
+
+
+ |
+
+
+
+
+ {{ view.project.start_at|datetime(format='dd MMM YYYY') }} | {{ view.project.start_at|datetime(format='HH:MM') }}
+ |
+
+
+
+ {{ view.project.description.html }}
+ |
+
+
+
+ {# Button : BEGIN #}
+ {{ cta_button(view.project.url_for(_external=true), gettext("Register") )}}
+ {# Button : END #}
+
+ {# {{ pinned_update(view, project) }} #}
+
+
+ |
+
+
+
+
+
+{%- endblock content -%}
diff --git a/funnel/templates/notifications/project_starting_email.html.jinja2 b/funnel/templates/notifications/project_starting_email.html.jinja2
index 4aeb13471..00cf90f19 100644
--- a/funnel/templates/notifications/project_starting_email.html.jinja2
+++ b/funnel/templates/notifications/project_starting_email.html.jinja2
@@ -1,15 +1,22 @@
{%- extends "notifications/layout_email.html.jinja2" -%}
-{%- from "notifications/macros_email.html.jinja2" import pinned_update -%}
+{%- from "notifications/macros_email.html.jinja2" import pinned_update, cta_button, rsvp_footer -%}
{%- block content -%}
-
- {%- trans project=view.project.joined_title, start_time=(view.session or view.project).start_at_localized|time -%}
- {{ project }} starts at {{ start_time }}
- {%- endtrans -%}
-
+
+
+ {%- trans project=view.project.joined_title, start_time=(view.session or view.project).start_at_localized|time -%}{{ project }} starts at {{ start_time }}{%- endtrans -%}
+ |
+
+
-{% trans %}Join now{% endtrans %}
+ {# Button : BEGIN #}
+ {{ cta_button(view.project.url_for(_external=true, **view.tracking_tags()), gettext("Join now") )}}
+ {# Button : END #}
-{{ pinned_update(view, view.project) }}
+ {{ pinned_update(view, view.rsvp.project) }}
+
+ {# Email body footer : BEGIN #}
+ {{ rsvp_footer(view, gettext("Cancel registration")) }}
+ {# Email body footer : END #}
{%- endblock content -%}
diff --git a/funnel/templates/notifications/proposal_received_email.html.jinja2 b/funnel/templates/notifications/proposal_received_email.html.jinja2
index 49d40d6ad..aac8b2b32 100644
--- a/funnel/templates/notifications/proposal_received_email.html.jinja2
+++ b/funnel/templates/notifications/proposal_received_email.html.jinja2
@@ -1,8 +1,16 @@
-{% extends "notifications/layout_email.html.jinja2" %}
-{% block content -%}
+{%- extends "notifications/layout_email.html.jinja2" -%}
+{%- from "notifications/macros_email.html.jinja2" import cta_button -%}
+{%- block content -%}
-{% trans project=project.joined_title, proposal=proposal.title %}Your project {{ project }} has a new submission: {{ proposal }}{% endtrans %}
+
+
+ {% trans project=project.joined_title, proposal=proposal.title %}Your project {{ project }} has a new submission: {{ proposal }}{% endtrans %}
+ |
+
+
-{% trans %}Submission page{% endtrans %}
+ {# Button : BEGIN #}
+ {{ cta_button(proposal.url_for(_external=true, **view.tracking_tags()), gettext("Submission page") )}}
+ {# Button : END #}
-{%- endblock content %}
+{%- endblock content -%}
diff --git a/funnel/templates/notifications/proposal_submitted_email.html.jinja2 b/funnel/templates/notifications/proposal_submitted_email.html.jinja2
index b856dc0e6..0e62b71c8 100644
--- a/funnel/templates/notifications/proposal_submitted_email.html.jinja2
+++ b/funnel/templates/notifications/proposal_submitted_email.html.jinja2
@@ -1,8 +1,16 @@
-{% extends "notifications/layout_email.html.jinja2" %}
-{% block content -%}
+{%- extends "notifications/layout_email.html.jinja2" -%}
+{%- from "notifications/macros_email.html.jinja2" import cta_button -%}
+{%- block content -%}
-{% trans project=project.joined_title, proposal=proposal.title %}You have submitted {{ proposal }} to the project {{ project }}{% endtrans %}
+
+
+ {% trans project=project.joined_title, proposal=proposal.title %}You have submitted {{ proposal }} to the project {{ project }}{% endtrans %}
+ |
+
+
-{% trans %}View submission{% endtrans %}
+ {# Button : BEGIN #}
+ {{ cta_button(proposal.url_for(_external=true, **view.tracking_tags()), gettext("View submission") )}}
+ {# Button : END #}
-{%- endblock content %}
+{%- endblock content -%}
diff --git a/funnel/templates/notifications/rsvp_no_email.html.jinja2 b/funnel/templates/notifications/rsvp_no_email.html.jinja2
index 967f10861..1bcc9f84b 100644
--- a/funnel/templates/notifications/rsvp_no_email.html.jinja2
+++ b/funnel/templates/notifications/rsvp_no_email.html.jinja2
@@ -1,8 +1,19 @@
{% extends "notifications/layout_email.html.jinja2" %}
+{%- from "notifications/macros_email.html.jinja2" import cta_button, rsvp_footer -%}
{% block content -%}
-{% trans project=view.rsvp.project.joined_title %}You have cancelled your registration for {{ project }}. If this was accidental, you can register again.{% endtrans %}
+
+
+ {% trans project=view.rsvp.project.joined_title %}You have cancelled your registration for {{ project }}. If this was accidental, you can register again.{% endtrans %}
+ |
+
-{% trans %}Project page{% endtrans %}
+ {# Button : BEGIN #}
+ {{ cta_button(view.rsvp.project.url_for(_external=true, **view.tracking_tags()), gettext("Project page") )}}
+ {# Button : END #}
+
+ {# Email body footer : BEGIN #}
+ {{ rsvp_footer(view, gettext("Register again")) }}
+ {# Email body footer : END #}
{%- endblock content %}
diff --git a/funnel/templates/notifications/rsvp_yes_email.html.jinja2 b/funnel/templates/notifications/rsvp_yes_email.html.jinja2
index 9528aec75..95703855e 100644
--- a/funnel/templates/notifications/rsvp_yes_email.html.jinja2
+++ b/funnel/templates/notifications/rsvp_yes_email.html.jinja2
@@ -1,15 +1,24 @@
{% extends "notifications/layout_email.html.jinja2" -%}
-{%- from "notifications/macros_email.html.jinja2" import pinned_update -%}
+{%- from "notifications/macros_email.html.jinja2" import pinned_update, cta_button, rsvp_footer -%}
{%- block content -%}
-{% trans project=view.rsvp.project.joined_title %}You have registered for {{ project }}{% endtrans %}
+
+
+ {% trans project=view.rsvp.project.joined_title %}You have registered for {{ project }}{% endtrans %}
+ {% with next_session_at=view.rsvp.project.next_session_at %}{% if next_session_at -%}
+ {% trans date_and_time=next_session_at|datetime(view.datetime_format) %}The next session in the schedule starts {{ date_and_time }}{% endtrans %}
+ {%- endif %}{% endwith %}
+ |
+
-{% with next_session_at=view.rsvp.project.next_session_at %}{% if next_session_at -%}
- {% trans date_and_time=next_session_at|datetime(view.datetime_format) %}The next session in the schedule starts {{ date_and_time }}{% endtrans %}
-{%- endif %}{% endwith %}
+ {# Button : BEGIN #}
+ {{ cta_button(view.rsvp.project.url_for(_external=true, **view.tracking_tags()), gettext("Project page") )}}
+ {# Button : END #}
-{% trans %}Project page{% endtrans %}
+ {{ pinned_update(view, view.rsvp.project) }}
-{{ pinned_update(view, view.rsvp.project) }}
+ {# Email body footer : BEGIN #}
+ {{ rsvp_footer(view, gettext("Cancel registration")) }}
+ {# Email body footer : END #}
{%- endblock content -%}
diff --git a/funnel/templates/notifications/update_new_email.html.jinja2 b/funnel/templates/notifications/update_new_email.html.jinja2
index 8a5a2a3a1..fb36c9ad4 100644
--- a/funnel/templates/notifications/update_new_email.html.jinja2
+++ b/funnel/templates/notifications/update_new_email.html.jinja2
@@ -1,12 +1,26 @@
-{% extends "notifications/layout_email.html.jinja2" %}
-{% block content %}
+{%- extends "notifications/layout_email.html.jinja2" -%}
+{%- from "notifications/macros_email.html.jinja2" import cta_button -%}
+{%- block content -%}
-{% trans actor=view.actor.pickername, project=view.update.project.joined_title, project_url=view.update.project.url_for() %}{{ actor }} posted an update in {{ project }}:{% endtrans %}
+
+
+ {% trans actor=view.actor.pickername, project=view.update.project.joined_title, project_url=view.update.project.url_for() %}{{ actor }} posted an update in {{ project }}:{% endtrans %}
+ |
+
+
+ |
+
+
+
+
+
+ {% trans update_body=view.update.body %}{{ update_body }}{% endtrans %}
+ |
+
+
-
+ {# Button : BEGIN #}
+ {{ cta_button(view.update.url_for(_external=true, **view.tracking_tags()), gettext("Read on the website") )}}
+ {# Button : END #}
-{{ view.update.body }}
-
-{% trans %}Read on the website{% endtrans %}
-
-{% endblock content %}
+{%- endblock content -%}
diff --git a/funnel/templates/notifications/user_password_set_email.html.jinja2 b/funnel/templates/notifications/user_password_set_email.html.jinja2
index e8e01b380..d8952141d 100644
--- a/funnel/templates/notifications/user_password_set_email.html.jinja2
+++ b/funnel/templates/notifications/user_password_set_email.html.jinja2
@@ -1,17 +1,17 @@
-{%- extends "notifications/layout_email.html.jinja2" %}
-
+{%- extends "notifications/layout_email.html.jinja2" -%}
+{%- from "notifications/macros_email.html.jinja2" import cta_button -%}
{%- block content -%}
-
- {%- trans %}Your password has been updated. If you did this, no further action is necessary.{% endtrans -%}
-
-
- {%- trans %}If this was not authorized, consider resetting with a more secure password. Contact support if further assistance is required.{% endtrans -%}
-
+
+
+ {% if view.email_heading %}{{ view.email_heading }}{% endif %}
+ {%- trans support_email=config['SITE_SUPPORT_EMAIL'] %}Your password has been updated. If you did this, no further action is necessary. If this was not authorized, consider resetting with a more secure password. Contact support if further assistance is required.{% endtrans -%}
+ |
+
+
-
- {% trans %}Reset password{% endtrans %}
- {% trans %}Contact support{% endtrans %}
-
+ {# Button : BEGIN #}
+ {{ cta_button(url_for('reset'), gettext("Reset password") )}}
+ {# Button : END #}
{%- endblock content -%}
diff --git a/funnel/views/notification.py b/funnel/views/notification.py
index 5cd22fd6f..741e34cf4 100644
--- a/funnel/views/notification.py
+++ b/funnel/views/notification.py
@@ -142,10 +142,18 @@ class MyNotificationView(NotificationView):
aliases: Dict[Literal['document', 'fragment'], str] = {}
#: Emoji prefix, for transports that support them
- emoji_prefix = ''
+ emoji_prefix: str = ''
+
+ #: Hero image for email
+ hero_image: Optional[str] = None
+
+ #: Email heading (not subject)
+ email_heading: Optional[str] = None
#: Reason specified in email templates. Subclasses MAY override
- reason = __("You are receiving this because you have an account at hasgeek.com")
+ reason: str = __(
+ "You are receiving this because you have an account at hasgeek.com"
+ )
#: Copies of reason per transport that can be overriden by subclasses using either
#: a property or an attribute
diff --git a/funnel/views/notifications/__init__.py b/funnel/views/notifications/__init__.py
index 6e4be94b0..f4c1df58d 100644
--- a/funnel/views/notifications/__init__.py
+++ b/funnel/views/notifications/__init__.py
@@ -8,6 +8,7 @@
comment_notification,
organization_membership_notification,
project_crew_notification,
+ project_published_notification,
project_starting_notification,
proposal_notification,
rsvp_notification,
diff --git a/funnel/views/notifications/account_notification.py b/funnel/views/notifications/account_notification.py
index 66a2e6eee..c93742b3b 100644
--- a/funnel/views/notifications/account_notification.py
+++ b/funnel/views/notifications/account_notification.py
@@ -4,7 +4,7 @@
from flask import render_template, url_for
-from baseframe import _
+from baseframe import _, __
from ... import app
from ...models import AccountPasswordNotification, User
@@ -20,6 +20,8 @@ class RenderAccountPasswordNotification(RenderNotification):
user: User
aliases = {'document': 'user'}
emoji_prefix = "⚠️ "
+ hero_image = 'https://images.hasgeek.com/embed/file/7a06845297f1416bb497fbcb2d167294?size=196x134'
+ email_heading = __("Password updated!")
@property
def actor(self):
diff --git a/funnel/views/notifications/comment_notification.py b/funnel/views/notifications/comment_notification.py
index 896cc535f..bfb04b932 100644
--- a/funnel/views/notifications/comment_notification.py
+++ b/funnel/views/notifications/comment_notification.py
@@ -33,6 +33,8 @@ class RenderCommentReportReceivedNotification(RenderNotification):
aliases = {'document': 'comment', 'fragment': 'report'}
emoji_prefix = "💩 "
reason = __("You are receiving this because you are a site admin")
+ hero_image = 'https://images.hasgeek.com/embed/file/f0458e8de65b4aca87633b45b410e7fb?size=190x150'
+ email_heading = __("Spam alert!")
def web(self) -> str:
return render_template(
@@ -70,6 +72,8 @@ class CommentNotification(RenderNotification):
comment: Comment
aliases = {'fragment': 'comment'}
emoji_prefix = "💬 "
+ hero_image = 'https://images.hasgeek.com/embed/file/3a9ffa34bbe54655ba88c8e4e77cfde9?size=196x165'
+ email_heading = __("New comment!")
@property
def actor(self) -> Union[User, DuckTypeUser]:
diff --git a/funnel/views/notifications/organization_membership_notification.py b/funnel/views/notifications/organization_membership_notification.py
index 7c3dda4e2..8b4403410 100644
--- a/funnel/views/notifications/organization_membership_notification.py
+++ b/funnel/views/notifications/organization_membership_notification.py
@@ -441,6 +441,8 @@ class RenderOrganizationAdminMembershipNotification(RenderShared, RenderNotifica
aliases = {'document': 'organization', 'fragment': 'membership'}
reason = __("You are receiving this because you are an admin of this organization")
+ hero_image = 'https://images.hasgeek.com/embed/file/466dbf43ee3f486f82f3117093aacec0?size=196x165'
+ email_heading = __("Membership granted!")
template_picker = grant_amend_templates
fragments_order_by = [OrganizationMembership.granted_at.desc()]
@@ -472,6 +474,8 @@ class RenderOrganizationAdminMembershipRevokedNotification(
aliases = {'document': 'organization', 'fragment': 'membership'}
reason = __("You are receiving this because you were an admin of this organization")
+ hero_image = 'https://images.hasgeek.com/embed/file/6303c4386e354c4989492be527e76332?size=196x156'
+ email_heading = __("Membership revoked")
template_picker = revoke_templates
fragments_order_by = [OrganizationMembership.revoked_at.desc()]
diff --git a/funnel/views/notifications/project_crew_notification.py b/funnel/views/notifications/project_crew_notification.py
index 5bafdf4b0..12b70f16d 100644
--- a/funnel/views/notifications/project_crew_notification.py
+++ b/funnel/views/notifications/project_crew_notification.py
@@ -774,6 +774,8 @@ class RenderProjectCrewMembershipNotification(RenderShared, RenderNotification):
"""Render a notification for project crew invite/add/amend."""
aliases = {'document': 'project', 'fragment': 'membership'}
+ hero_image = 'https://images.hasgeek.com/embed/file/c358d0930d00425da8a8f159e57b0138?size=196x163'
+ email_heading = __("Crew membership granted!")
fragments_order_by = [ProjectCrewMembership.granted_at.desc()]
template_picker = grant_amend_templates
@@ -799,6 +801,8 @@ class RenderProjectCrewMembershipRevokedNotification(RenderShared, RenderNotific
"""Render a notification for project crew revocation."""
aliases = {'document': 'project', 'fragment': 'membership'}
+ hero_image = 'https://images.hasgeek.com/embed/file/be2dbf5f29c044e2be3c02885dd0afda?size=196x140'
+ email_heading = __("Crew membership revoked")
template_picker = revoke_templates
def membership_actor(
diff --git a/funnel/views/notifications/project_published_notification.py b/funnel/views/notifications/project_published_notification.py
new file mode 100644
index 000000000..65f30a911
--- /dev/null
+++ b/funnel/views/notifications/project_published_notification.py
@@ -0,0 +1,59 @@
+"""Project update notifications."""
+
+from __future__ import annotations
+
+from flask import render_template
+
+from baseframe import _, __
+
+from ...models import Project, ProjectPublishedNotification
+from ...transports.sms import TwoLineTemplate
+from ..helpers import shortlink
+from ..notification import RenderNotification
+
+
+@ProjectPublishedNotification.renderer
+class RenderProjectPublishedNotification(RenderNotification):
+ """Notify crew and participants when the project has a new update."""
+
+ project: Project
+ aliases = {'document': 'project'}
+ emoji_prefix = "📰 "
+ reason = __(
+ "You are receiving this because you have registered for this or related"
+ " projects"
+ )
+
+ @property
+ def actor(self):
+ """
+ Return author of the update.
+
+ Updates may be written by one user and published by another. The notification's
+ default actor is the publisher as they caused it to be dispatched, but in this
+ case the actor of interest is the author of the update.
+ """
+ return self.project.user
+
+ def web(self):
+ return render_template('notifications/update_new_web.html.jinja2', view=self)
+
+ def email_subject(self):
+ return self.emoji_prefix + _("{update} ({project})").format(
+ update=self.project.title, project=self.project.joined_title
+ )
+
+ def email_content(self):
+ return render_template(
+ 'notifications/project_published_email.html.jinja2', view=self
+ )
+
+ def sms(self) -> TwoLineTemplate:
+ return TwoLineTemplate(
+ text1=_("Update in {project}:").format(project=self.project.joined_title),
+ text2=self.project.title,
+ url=shortlink(
+ self.update.url_for(_external=True, **self.tracking_tags('sms')),
+ shorter=True,
+ ),
+ )
diff --git a/funnel/views/notifications/project_starting_notification.py b/funnel/views/notifications/project_starting_notification.py
index 18251ca36..6560576c7 100644
--- a/funnel/views/notifications/project_starting_notification.py
+++ b/funnel/views/notifications/project_starting_notification.py
@@ -42,6 +42,8 @@ class RenderProjectStartingNotification(RenderNotification):
aliases = {'document': 'project', 'fragment': 'session'}
emoji_prefix = "⏰ "
reason = __("You are receiving this because you have registered for this project")
+ hero_image = 'https://images.hasgeek.com/embed/file/fa17b184099345dd8dcd1b560975d48e?size=196x151'
+ email_heading = __("Session starting soon!")
def web(self) -> str:
return render_template(
diff --git a/funnel/views/notifications/proposal_notification.py b/funnel/views/notifications/proposal_notification.py
index 5df16ca81..256473ed5 100644
--- a/funnel/views/notifications/proposal_notification.py
+++ b/funnel/views/notifications/proposal_notification.py
@@ -27,6 +27,8 @@ class RenderProposalReceivedNotification(RenderNotification):
aliases = {'document': 'project', 'fragment': 'proposal'}
emoji_prefix = "📥 "
reason = __("You are receiving this because you are an editor of this project")
+ hero_image = 'https://images.hasgeek.com/embed/file/6fbd959c2e0b471581ccb4a402261151?size=196x151'
+ email_heading = __("New submission!")
fragments_order_by = [Proposal.datetime.desc()]
fragments_query_options = [
@@ -77,6 +79,8 @@ class RenderProposalSubmittedNotification(RenderNotification):
aliases = {'document': 'proposal'}
emoji_prefix = "📤 "
reason = __("You are receiving this because you made this submission")
+ hero_image = 'https://images.hasgeek.com/embed/file/05b9bd5c30c343f6964ad6f17822e268?size=196x130'
+ email_heading = __("Proposal sumbitted!")
def web(self):
return render_template(
diff --git a/funnel/views/notifications/rsvp_notification.py b/funnel/views/notifications/rsvp_notification.py
index fa25ce13e..05840c954 100644
--- a/funnel/views/notifications/rsvp_notification.py
+++ b/funnel/views/notifications/rsvp_notification.py
@@ -102,6 +102,8 @@ class RenderRegistrationConfirmationNotification(RegistrationBase, RenderNotific
aliases = {'document': 'rsvp'}
reason = __("You are receiving this because you have registered for this project")
+ hero_image = 'https://images.hasgeek.com/embed/file/c9ccfc1899614d56b90b4fa4d0453ff0?size=196x138'
+ email_heading = __("Registration confirmed!")
datetime_format = "EEE, dd MMM yyyy, hh:mm a"
datetime_format_sms = "EEE, dd MMM, hh:mm a"
@@ -153,6 +155,8 @@ class RenderRegistrationCancellationNotification(RegistrationBase, RenderNotific
aliases = {'document': 'rsvp'}
reason = __("You are receiving this because you had registered for this project")
+ hero_image = 'https://images.hasgeek.com/embed/file/3d92f4f213644d50ae11cc2039d3fbc9?size=196x133'
+ email_heading = __("Registration cancelled")
def web(self) -> str:
return render_template('notifications/rsvp_no_web.html.jinja2', view=self)
diff --git a/funnel/views/notifications/update_notification.py b/funnel/views/notifications/update_notification.py
index 1b8bb8f25..6514b7ff1 100644
--- a/funnel/views/notifications/update_notification.py
+++ b/funnel/views/notifications/update_notification.py
@@ -23,6 +23,8 @@ class RenderNewUpdateNotification(RenderNotification):
"You are receiving this because you have registered for this or related"
" projects"
)
+ hero_image = 'https://images.hasgeek.com/embed/file/cb7aaafd195840ec99376cf23f5136b8?size=196x190'
+ email_heading = __("New update!")
@property
def actor(self):
diff --git a/funnel/views/project.py b/funnel/views/project.py
index aec102f66..9b9c3201c 100644
--- a/funnel/views/project.py
+++ b/funnel/views/project.py
@@ -39,6 +39,7 @@
RSVP_STATUS,
Profile,
Project,
+ ProjectPublishedNotification,
RegistrationCancellationNotification,
RegistrationConfirmationNotification,
Rsvp,
@@ -559,6 +560,7 @@ def transition(self) -> ReturnView:
transition() # call the transition
db.session.commit()
flash(transition.data['message'], 'success')
+ dispatch_notification(ProjectPublishedNotification(document=self.obj))
else:
flash(_("Invalid transition for this project"), 'error')
abort(403)