Send email notifications when a participation is approved/rejected

Also handles participant status :rejected instead of deleting the
participation

Closes #164

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel
2019-09-30 13:48:47 +02:00
parent d30b2fa147
commit 5b4f1c271a
47 changed files with 3092 additions and 484 deletions

View File

@@ -70,6 +70,7 @@ defmodule Mobilizon.Events do
defenum(ParticipantRole, :participant_role, [
:not_approved,
:rejected,
:participant,
:moderator,
:administrator,
@@ -718,6 +719,17 @@ defmodule Mobilizon.Events do
|> Repo.aggregate(:count, :id)
end
@doc """
Counts rejected participants.
"""
@spec count_rejected_participants(integer | String.t()) :: integer
def count_rejected_participants(event_id) do
event_id
|> count_participants_query()
|> filter_rejected_role()
|> Repo.aggregate(:count, :id)
end
@doc """
Gets the default participant role depending on the event join options.
"""
@@ -1361,7 +1373,7 @@ defmodule Mobilizon.Events do
@spec filter_approved_role(Ecto.Query.t()) :: Ecto.Query.t()
defp filter_approved_role(query) do
from(p in query, where: p.role != ^:not_approved)
from(p in query, where: p.role not in ^[:not_approved, :rejected])
end
@spec filter_unapproved_role(Ecto.Query.t()) :: Ecto.Query.t()
@@ -1369,6 +1381,11 @@ defmodule Mobilizon.Events do
from(p in query, where: p.role == ^:not_approved)
end
@spec filter_rejected_role(Ecto.Query.t()) :: Ecto.Query.t()
defp filter_rejected_role(query) do
from(p in query, where: p.role == ^:rejected)
end
@spec filter_role(Ecto.Query.t(), list(atom())) :: Ecto.Query.t()
defp filter_role(query, []), do: query

View File

@@ -7,6 +7,7 @@ defmodule MobilizonWeb.API.Participations do
alias Mobilizon.Events
alias Mobilizon.Events.{Event, Participant}
alias Mobilizon.Service.ActivityPub
alias MobilizonWeb.Email.Participation
@spec join(Event.t(), Actor.t()) :: {:ok, Participant.t()}
def join(%Event{id: event_id} = event, %Actor{id: actor_id} = actor) do
@@ -22,10 +23,19 @@ defmodule MobilizonWeb.API.Participations do
end
end
def accept(
%Participant{} = participation,
%Actor{} = moderator
) do
@doc """
Update participation status
"""
def update(%Participant{} = participation, %Actor{} = moderator, :participant),
do: accept(participation, moderator)
def update(%Participant{} = participation, %Actor{} = moderator, :rejected),
do: reject(participation, moderator)
defp accept(
%Participant{} = participation,
%Actor{} = moderator
) do
with {:ok, activity, _} <-
ActivityPub.accept(
%{
@@ -36,15 +46,16 @@ defmodule MobilizonWeb.API.Participations do
"#{MobilizonWeb.Endpoint.url()}/accept/join/#{participation.id}"
),
{:ok, %Participant{role: :participant} = participation} <-
Events.update_participant(participation, %{"role" => :participant}) do
Events.update_participant(participation, %{"role" => :participant}),
:ok <- Participation.send_emails_to_local_user(participation) do
{:ok, activity, participation}
end
end
def reject(
%Participant{} = participation,
%Actor{} = moderator
) do
defp reject(
%Participant{} = participation,
%Actor{} = moderator
) do
with {:ok, activity, _} <-
ActivityPub.reject(
%{
@@ -54,8 +65,9 @@ defmodule MobilizonWeb.API.Participations do
},
"#{MobilizonWeb.Endpoint.url()}/reject/join/#{participation.id}"
),
{:ok, %Participant{} = participation} <-
Events.delete_participant(participation) do
{:ok, %Participant{role: :rejected} = participation} <-
Events.update_participant(participation, %{"role" => :rejected}),
:ok <- Participation.send_emails_to_local_user(participation) do
{:ok, activity, participation}
end
end

View File

@@ -0,0 +1,84 @@
defmodule MobilizonWeb.Email.Participation do
@moduledoc """
Handles emails sent about participation.
"""
use Bamboo.Phoenix, view: MobilizonWeb.EmailView
import Bamboo.Phoenix
import MobilizonWeb.Gettext
alias Mobilizon.Users.User
alias Mobilizon.Actors.Actor
alias Mobilizon.Events.Participant
alias MobilizonWeb.Email
@doc """
Send emails to local user
"""
def send_emails_to_local_user(
%Participant{actor: %Actor{user_id: nil} = _actor} = _participation
),
do: :ok
@doc """
Send emails to local user
"""
def send_emails_to_local_user(
%Participant{actor: %Actor{user_id: user_id} = _actor} = participation
) do
with %User{} = user <- Mobilizon.Users.get_user!(user_id) do
user
|> participation_updated(participation)
|> Email.Mailer.deliver_later()
:ok
end
end
@spec participation_updated(User.t(), Participant.t(), String.t()) :: Bamboo.Email.t()
def participation_updated(user, participant, locale \\ "en")
def participation_updated(
%User{email: email},
%Participant{event: event, role: :rejected},
locale
) do
Gettext.put_locale(locale)
subject =
gettext(
"Your participation to event %{title} has been rejected",
title: event.title
)
Email.base_email(to: email, subject: subject)
|> assign(:locale, locale)
|> assign(:event, event)
|> assign(:subject, subject)
|> render(:event_participation_rejected)
end
@spec participation_updated(User.t(), Participant.t(), String.t()) :: Bamboo.Email.t()
def participation_updated(
%User{email: email},
%Participant{event: event, role: :participant},
locale
) do
Gettext.put_locale(locale)
subject =
gettext(
"Your participation to event %{title} has been approved",
title: event.title
)
Email.base_email(to: email, subject: subject)
|> assign(:locale, locale)
|> assign(:event, event)
|> assign(:subject, subject)
|> render(:event_participation_approved)
end
end

View File

@@ -83,7 +83,8 @@ defmodule MobilizonWeb.Resolvers.Event do
{:ok,
%{
approved: Mobilizon.Events.count_approved_participants(id),
unapproved: Mobilizon.Events.count_unapproved_participants(id)
unapproved: Mobilizon.Events.count_unapproved_participants(id),
rejected: Mobilizon.Events.count_rejected_participants(id)
}}
end
@@ -202,9 +203,9 @@ defmodule MobilizonWeb.Resolvers.Event do
{:error, "You need to be logged-in to leave an event"}
end
def accept_participation(
def update_participation(
_parent,
%{id: participation_id, moderator_actor_id: moderator_actor_id},
%{id: participation_id, moderator_actor_id: moderator_actor_id, role: new_role},
%{
context: %{
current_user: user
@@ -214,14 +215,15 @@ defmodule MobilizonWeb.Resolvers.Event do
# Check that moderator provided is rightly authenticated
with {:is_owned, moderator_actor} <- User.owns_actor(user, moderator_actor_id),
# Check that participation already exists
{:has_participation, %Participant{role: :not_approved} = participation} <-
{:has_participation, %Participant{role: old_role} = participation} <-
{:has_participation, Mobilizon.Events.get_participant(participation_id)},
{:same_role, false} <- {:same_role, new_role == old_role},
# Check that moderator has right
{:actor_approve_permission, true} <-
{:actor_approve_permission,
Mobilizon.Events.moderator_for_event?(participation.event.id, moderator_actor_id)},
{:ok, _activity, participation} <-
MobilizonWeb.API.Participations.accept(participation, moderator_actor) do
MobilizonWeb.API.Participations.update(participation, moderator_actor, new_role) do
{:ok, participation}
else
{:is_owned, nil} ->
@@ -234,55 +236,14 @@ defmodule MobilizonWeb.Resolvers.Event do
{:actor_approve_permission, _} ->
{:error, "Provided moderator actor ID doesn't have permission on this event"}
{:same_role, true} ->
{:error, "Participant already has role #{new_role}"}
{:error, :participant_not_found} ->
{:error, "Participant not found"}
end
end
def reject_participation(
_parent,
%{id: participation_id, moderator_actor_id: moderator_actor_id},
%{
context: %{
current_user: user
}
}
) do
# Check that moderator provided is rightly authenticated
with {:is_owned, moderator_actor} <- User.owns_actor(user, moderator_actor_id),
# Check that participation really exists
{:has_participation, %Participant{} = participation} <-
{:has_participation, Mobilizon.Events.get_participant(participation_id)},
# Check that moderator has right
{:actor_approve_permission, true} <-
{:actor_approve_permission,
Mobilizon.Events.moderator_for_event?(participation.event.id, moderator_actor_id)},
{:ok, _activity, participation} <-
MobilizonWeb.API.Participations.reject(participation, moderator_actor) do
{
:ok,
%{
id: participation.id,
event: %{
id: participation.event.id
},
actor: %{
id: participation.actor.id
}
}
}
else
{:is_owned, nil} ->
{:error, "Moderator Actor ID is not owned by authenticated user"}
{:actor_approve_permission, _} ->
{:error, "Provided moderator actor ID doesn't have permission on this event"}
{:has_participation, nil} ->
{:error, "Participant not found"}
end
end
@doc """
Create an event
"""

View File

@@ -111,6 +111,7 @@ defmodule MobilizonWeb.Schema.EventType do
object :participant_stats do
field(:approved, :integer, description: "The number of approved participants")
field(:unapproved, :integer, description: "The number of unapproved participants")
field(:rejected, :integer, description: "The number of rejected participants")
end
object :event_offer do

View File

@@ -38,6 +38,7 @@ defmodule MobilizonWeb.Schema.Events.ParticipantType do
value(:moderator)
value(:administrator)
value(:creator)
value(:rejected)
end
@desc "Represents a deleted participant"
@@ -65,19 +66,12 @@ defmodule MobilizonWeb.Schema.Events.ParticipantType do
end
@desc "Accept a participation"
field :accept_participation, :participant do
field :update_participation, :participant do
arg(:id, non_null(:id))
arg(:role, non_null(:participant_role_enum))
arg(:moderator_actor_id, non_null(:id))
resolve(&Resolvers.Event.accept_participation/3)
end
@desc "Reject a participation"
field :reject_participation, :deleted_participant do
arg(:id, non_null(:id))
arg(:moderator_actor_id, non_null(:id))
resolve(&Resolvers.Event.reject_participation/3)
resolve(&Resolvers.Event.update_participation/3)
end
end
end

View File

@@ -0,0 +1,81 @@
<!-- HERO -->
<tr>
<td bgcolor="#424056" align="center" style="padding: 0px 10px 0px 10px;">
<!--[if (gte mso 9)|(IE)]>
<table align="center" border="0" cellspacing="0" cellpadding="0" width="600">
<tr>
<td align="center" valign="top" width="600">
<![endif]-->
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;" >
<tr>
<td bgcolor="#ffffff" align="center" valign="top" style="padding: 40px 20px 20px 20px; border-radius: 4px 4px 0px 0px; color: #111111; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 48px; font-weight: 400; letter-spacing: 4px; line-height: 48px;">
<h1 style="font-size: 48px; font-weight: 400; margin: 0;">
<%= gettext "All good!" %>
</h1>
</td>
</tr>
</table>
<!--[if (gte mso 9)|(IE)]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
<!-- COPY BLOCK -->
<tr>
<td bgcolor="#f4f4f4" align="center" style="padding: 0px 10px 0px 10px;">
<!--[if (gte mso 9)|(IE)]>
<table align="center" border="0" cellspacing="0" cellpadding="0" width="600">
<tr>
<td align="center" valign="top" width="600">
<![endif]-->
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;" >
<!-- COPY -->
<tr>
<td bgcolor="#ffffff" align="left" style="padding: 20px 30px 0px 30px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
<p style="margin: 0;">
<%= gettext "You requested to participate in event %{title}", title: @event.title %>
</p>
</td>
</tr>
<tr>
<td bgcolor="#ffffff" align="left" style="padding: 20px 30px 40px 30px; color: #777777; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
<p style="margin: 0">
<%= gettext "An organizer just approved your participation. You're now going to this event!" %>
</p>
</td>
</tr>
<!-- BULLETPROOF BUTTON -->
<tr>
<td bgcolor="#ffffff" align="left">
<table width="100%" border="0" cellspacing="0" cellpadding="0">
<tr>
<td bgcolor="#ffffff" align="center" style="padding: 20px 30px 60px 30px;">
<table border="0" cellspacing="0" cellpadding="0">
<tr>
<td align="center" style="border-radius: 3px;" bgcolor="#424056"><a href="<%= page_url(MobilizonWeb.Endpoint, :event, @event.id) %>" target="_blank" style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; color: #ffffff; text-decoration: none; padding: 15px 25px; border-radius: 2px; border: 1px solid #424056; display: inline-block;">
<%= gettext "Go to event page" %>
</a></td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td bgcolor="#ffffff" align="left" style="padding: 20px 30px 40px 30px; color: #777777; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: 400; line-height: 20px;" >
<p style="margin: 0">
<%= gettext "If you need to cancel your participation, just access the event page through link above and click on the participation button." %>
</p>
</td>
</tr>
</table>
<!--[if (gte mso 9)|(IE)]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>

View File

@@ -0,0 +1,11 @@
<%= gettext "Participation approved" %>
==
<%= gettext "You requested to participate in event %{title}.", title: @event.title %>
<%= gettext "An organizer just approved your participation. You're now going to this event!" %>
<%= page_url(MobilizonWeb.Endpoint, :event, @event.id) %>
<%= gettext "If you need to cancel your participation, just access the previous link and click on the participation button." %>

View File

@@ -0,0 +1,56 @@
<!-- HERO -->
<tr>
<td bgcolor="#424056" align="center" style="padding: 0px 10px 0px 10px;">
<!--[if (gte mso 9)|(IE)]>
<table align="center" border="0" cellspacing="0" cellpadding="0" width="600">
<tr>
<td align="center" valign="top" width="600">
<![endif]-->
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;" >
<tr>
<td bgcolor="#ffffff" align="center" valign="top" style="padding: 40px 20px 20px 20px; border-radius: 4px 4px 0px 0px; color: #111111; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 48px; font-weight: 400; letter-spacing: 4px; line-height: 48px;">
<h1 style="font-size: 48px; font-weight: 400; margin: 0;">
<%= gettext "Sorry!" %>
</h1>
</td>
</tr>
</table>
<!--[if (gte mso 9)|(IE)]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
<!-- COPY BLOCK -->
<tr>
<td bgcolor="#f4f4f4" align="center" style="padding: 0px 10px 0px 10px;">
<!--[if (gte mso 9)|(IE)]>
<table align="center" border="0" cellspacing="0" cellpadding="0" width="600">
<tr>
<td align="center" valign="top" width="600">
<![endif]-->
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;" >
<!-- COPY -->
<tr>
<td bgcolor="#ffffff" align="left" style="padding: 20px 30px 0px 30px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
<p style="margin: 0;">
<%= gettext "You requested to participate in event %{title}", title: @event.title %>
</p>
</td>
</tr>
<tr>
<td bgcolor="#ffffff" align="left" style="padding: 20px 30px 40px 30px; color: #777777; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
<p style="margin: 0">
<%= gettext "Unfortunately, the organizers rejected your participation." %>
</p>
</td>
</tr>
</table>
<!--[if (gte mso 9)|(IE)]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>

View File

@@ -0,0 +1,7 @@
<%= gettext "Participation rejected" %>
==
<%= gettext "You requested to participate in event %{title}.", title: @event.title %>
<%= gettext "Unfortunately, the organizers rejected your participation." %>

View File

@@ -8,7 +8,6 @@ defmodule Mobilizon.Service.ActivityPub.Activity do
local: boolean,
actor: Actor.t(),
recipients: [String.t()]
# notifications: [???]
}
defstruct [
@@ -16,6 +15,5 @@ defmodule Mobilizon.Service.ActivityPub.Activity do
:local,
:actor,
:recipients
# :notifications
]
end

View File

@@ -454,7 +454,8 @@ defmodule Mobilizon.Service.ActivityPub do
{:only_organizer, Participant.is_not_only_organizer(event_id, actor_id)},
{:ok, %Participant{} = participant} <-
Mobilizon.Events.get_participant(event_id, actor_id),
{:ok, %Participant{} = participant} <- Mobilizon.Events.delete_participant(participant),
{:ok, %Participant{} = participant} <-
Events.delete_participant(participant),
leave_data <- %{
"type" => "Leave",
# If it's an exclusion it should be something else

View File

@@ -14,6 +14,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
alias Mobilizon.Events.{Comment, Event, Participant}
alias Mobilizon.Service.ActivityPub
alias Mobilizon.Service.ActivityPub.{Converter, Convertible, Utils, Visibility}
alias MobilizonWeb.Email.Participation
require Logger
@@ -543,10 +544,8 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
end
end
@doc """
Handle incoming `Accept` activities wrapping a `Join` activity on an event
"""
def do_handle_incoming_accept_join(join_object, %Actor{} = actor_accepting) do
# Handle incoming `Accept` activities wrapping a `Join` activity on an event
defp do_handle_incoming_accept_join(join_object, %Actor{} = actor_accepting) do
with {:join_event,
{:ok,
%Participant{role: :not_approved, actor: actor, id: join_id, event: event} =
@@ -566,7 +565,9 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
"#{MobilizonWeb.Endpoint.url()}/accept/join/#{join_id}"
),
{:ok, %Participant{role: :participant}} <-
Events.update_participant(participant, %{"role" => :participant}) do
Events.update_participant(participant, %{"role" => :participant}),
:ok <-
Participation.send_emails_to_local_user(participant) do
{:ok, activity, participant}
else
{:join_event, {:ok, %Participant{role: :participant}}} ->
@@ -591,10 +592,8 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
end
end
@doc """
Handle incoming `Reject` activities wrapping a `Join` activity on an event
"""
def do_handle_incoming_reject_join(join_object, %Actor{} = actor_accepting) do
# Handle incoming `Reject` activities wrapping a `Join` activity on an event
defp do_handle_incoming_reject_join(join_object, %Actor{} = actor_accepting) do
with {:join_event,
{:ok,
%Participant{role: :not_approved, actor: actor, id: join_id, event: event} =
@@ -613,8 +612,9 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
},
"#{MobilizonWeb.Endpoint.url()}/reject/join/#{join_id}"
),
{:ok, %Participant{}} <-
Events.delete_participant(participant) do
{:ok, %Participant{role: :rejected} = participant} <-
Events.update_participant(participant, %{"role" => :rejected}),
:ok <- Participation.send_emails_to_local_user(participant) do
{:ok, activity, participant}
else
{:join_event, {:ok, %Participant{role: :participant}}} ->