diff --git a/funnel/models/__init__.py b/funnel/models/__init__.py
index 90e801c59..947e26912 100644
--- a/funnel/models/__init__.py
+++ b/funnel/models/__init__.py
@@ -131,6 +131,7 @@
"ProjectStartingNotification",
"ProjectTomorrowNotification",
"ProjectUpdateNotification",
+ "ProjectPublishedNotification",
"Proposal",
"ProposalLabelProxy",
"ProposalLabelProxyWrapper",
diff --git a/funnel/models/__init__.pyi b/funnel/models/__init__.pyi
index 6752aa5cc..198e022e5 100644
--- a/funnel/models/__init__.pyi
+++ b/funnel/models/__init__.pyi
@@ -176,6 +176,7 @@ from .notification_types import (
OrganizationAdminMembershipRevokedNotification,
ProjectCrewMembershipNotification,
ProjectCrewMembershipRevokedNotification,
+ ProjectPublishedNotification,
ProjectStartingNotification,
ProjectTomorrowNotification,
ProjectUpdateNotification,
@@ -354,6 +355,7 @@ __all__ = [
"ProjectRsvpStateEnum",
"ProjectSponsorMembership",
"ProjectStartingNotification",
+ "ProjectPublishedNotification",
"ProjectTomorrowNotification",
"ProjectUpdateNotification",
"Proposal",
diff --git a/funnel/models/notification_types.py b/funnel/models/notification_types.py
index 1b0467e37..ef2c492e8 100644
--- a/funnel/models/notification_types.py
+++ b/funnel/models/notification_types.py
@@ -34,6 +34,7 @@
'RegistrationCancellationNotification',
'RegistrationConfirmationNotification',
'ProjectStartingNotification',
+ 'ProjectPublishedNotification',
'ProjectTomorrowNotification',
'OrganizationAdminMembershipNotification',
'OrganizationAdminMembershipRevokedNotification',
@@ -69,6 +70,15 @@ def preference_context(self) -> Account:
return self.document # type: ignore[attr-defined]
+class DocumentIsProfile:
+ """Mixin class for notifications on the profile."""
+
+ @property
+ def preference_context(self) -> Account:
+ """Return the document as the preference context."""
+ return self.document # type: ignore[attr-defined]
+
+
# --- Account notifications ------------------------------------------------------------
@@ -197,6 +207,21 @@ class ProjectTomorrowNotification(
# This is a notification triggered without an actor
+class ProjectPublishedNotification(
+ DocumentIsAccount, Notification[Account, Project], type='project_published'
+):
+ """Notification of a newly published project."""
+
+ category = notification_categories.participant
+ title = __("When a project is published")
+ description = __(
+ "Notifies all members of a account when a new project is published"
+ )
+
+ roles = ['project_crew', 'account_participant']
+ exclude_actor = False # Send to everyone including the actor
+
+
# --- Comment notifications ------------------------------------------------------------
diff --git a/funnel/models/project.py b/funnel/models/project.py
index 13f39d60b..31527437f 100644
--- a/funnel/models/project.py
+++ b/funnel/models/project.py
@@ -720,14 +720,17 @@ def _(self) -> Iterable[Account]:
message=__("Submissions will be accepted until the optional closing date"),
type='success',
)
- def open_cfp(self) -> None:
+ def open_cfp(self) -> bool:
"""Change state to accept submissions."""
+ first_opened = False
# If closing date is in the past, remove it
if self.cfp_end_at is not None and self.cfp_end_at <= utcnow():
self.cfp_end_at = None
# If opening date is not set, set it
if self.cfp_start_at is None:
self.cfp_start_at = sa.func.utcnow()
+ first_opened = True
+ return first_opened
@with_roles(call={'editor'}) # skipcq: PTC-W0049
@cfp_state.transition(
diff --git a/funnel/templates/notifications/layout_email.html.jinja2 b/funnel/templates/notifications/layout_email.html.jinja2
index d9a3dba2c..206f9c7b0 100644
--- a/funnel/templates/notifications/layout_email.html.jinja2
+++ b/funnel/templates/notifications/layout_email.html.jinja2
@@ -355,7 +355,7 @@
@@ -365,9 +365,9 @@
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.
#}
-
+
@@ -443,7 +443,7 @@
{# Email content : BEGIN #}
- {% if view %}
+ {% if view.hero_image %}
{# Hero image centered : BEGIN #}
{%- if view.hero_image %}
{{ hero_image(view.hero_image, view.email_heading or '') }}
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..6b5375a45
--- /dev/null
+++ b/funnel/templates/notifications/project_published_email.html.jinja2
@@ -0,0 +1,125 @@
+{%- 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/views/notifications/__init__.py b/funnel/views/notifications/__init__.py
index bd81e1e70..e2064ce6f 100644
--- a/funnel/views/notifications/__init__.py
+++ b/funnel/views/notifications/__init__.py
@@ -9,6 +9,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/project_published_notification.py b/funnel/views/notifications/project_published_notification.py
new file mode 100644
index 000000000..a37a59632
--- /dev/null
+++ b/funnel/views/notifications/project_published_notification.py
@@ -0,0 +1,69 @@
+"""Project published notification."""
+
+from __future__ import annotations
+
+from flask import render_template
+
+from baseframe import _, __
+
+from ...models import Project, ProjectPublishedNotification
+from ...transports.sms import SmsTemplate
+from ..helpers import shortlink
+from ..notification import RenderNotification
+from .mixins import TemplateVarMixin
+
+
+class ProjectPublishedTemplate(TemplateVarMixin, SmsTemplate):
+ """DLT registered template for Project published."""
+
+ registered_template = (
+ "{#var#}, whose event you previously registered for, has just announced"
+ " {#var#}. Details here: {#var#}\n\nhttps://bye.li to stop -Hasgeek"
+ )
+ template = (
+ "{account}, whose event you previously registered for, has just announced"
+ " {project}. Details here: {url}\n\nhttps://bye.li to stop -Hasgeek"
+ )
+ plaintext_template = "{account} has published a new project: {url}"
+
+ url: str
+
+
+@ProjectPublishedNotification.renderer
+class RenderProjectPublishedNotification(RenderNotification):
+ """Notify account followers when a new project is published."""
+
+ project: Project
+ aliases = {'document': 'profile', 'fragment': 'project'}
+ emoji_prefix = "📰 "
+ reason = __(
+ "You are receiving this because you have registered for this or related"
+ " projects"
+ )
+
+ @property
+ def actor(self):
+ return self.project.created_by
+
+ def web(self):
+ return render_template('notifications/update_new_web.html.jinja2', view=self)
+
+ def email_subject(self):
+ return self.emoji_prefix + _("{title} ({project})").format(
+ title=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) -> ProjectPublishedTemplate:
+ return ProjectPublishedTemplate(
+ profile=self.project.account,
+ project=self.project,
+ url=shortlink(
+ self.project.url_for(_external=True, **self.tracking_tags('sms')),
+ shorter=True,
+ ),
+ )
diff --git a/funnel/views/project.py b/funnel/views/project.py
index 1fa9cc777..2367a22ed 100644
--- a/funnel/views/project.py
+++ b/funnel/views/project.py
@@ -33,6 +33,7 @@
from ..models import (
Account,
Project,
+ ProjectPublishedNotification,
ProjectRsvpStateEnum,
RegistrationCancellationNotification,
RegistrationConfirmationNotification,
@@ -642,15 +643,22 @@ def edit_boxoffice_data(self) -> ReturnView:
def transition(self) -> ReturnView:
"""Change project's state."""
transition_form = ProjectTransitionForm(obj=self.obj)
- if (
- transition_form.validate_on_submit()
- ): # check if the provided transition is valid
+ # Check if the provided transition is valid
+ if transition_form.validate_on_submit():
transition = getattr(
self.obj.current_access(), transition_form.transition.data
)
- transition() # call the transition
+ first_time_flag: bool | None = transition() # call the transition
db.session.commit()
flash(transition.data['message'], 'success')
+ if transition_form.transition.data == 'publish' and first_time_flag:
+ dispatch_notification(
+ ProjectPublishedNotification(
+ document=self.obj.account, fragment=self.obj
+ )
+ )
+ # If there's a future notification for CFP open, it can be dispatched here
+ # elif transition_form.transition.data == 'open_cfp' and first_time_flag:
else:
flash(_("Invalid transition for this project"), 'error')
abort(403)