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 #} + + + + + + + + +

{% trans %}If you did not ask for this, you may safely ignore this email.{% endtrans %}

+ + {%- 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 #} + + + + + + + + +

{% trans %}If you did not ask for this, you may safely ignore this email.{% endtrans %}

+ + {%- 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' -%} -

{{ view.document.joined_title }}

- {%- elif view.notification.document_type == 'proposal' -%} -

{{ view.document.title }}

- {%- elif view.notification.document_type == 'comment' -%} -

{% trans %}You wrote:{% endtrans %}

-
{{ view.document.message }}
- {%- endif %} + + + {%- if view.notification.document_type == 'project' -%} +

{{ view.document.joined_title }}

+ {%- elif view.notification.document_type == 'proposal' -%} +

{{ view.document.title }}

+ {%- 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 %}
-
{% block footer %} - {%- if view %} -
-

- {{ view.reason_email }} - • - {% trans %}Unsubscribe or manage preferences{% endtrans %} -

- {%- endif %} - {% endblock footer %}
- + +
+ + + {# + 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 #} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + +
+ + +
+ {# 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.title }}

+ {{ 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 -%} -

-

{{ update.title }}

+{%- macro hero_image(img_url, alt_text) -%} + + + {{ 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 %} + {{ view.project.title }} + {%- else %} + {{ view.project.title }} + {% endif %} + + + + +
+ {%- if view.project.profile.logo_url.url %} + + {{ view.project.profile.title }} + + {% endif %} + {{ view.project.profile.title }} +
+ + + + +

{{ view.project.title }}

+

{{ 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) }} #} + + + + + + {{ view.reason }} + + +{%- 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_title=view.update.title %}{{ update_title }}{% endtrans %}

+ {% trans update_body=view.update.body %}{{ update_body }}{% endtrans %} + + +
-

{{ view.update.title }}

+ {# 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)