feat: add links to cancel anonymous participations in emails
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
parent
b315e1d7ff
commit
9e6b232a78
|
@ -176,7 +176,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Participant do
|
||||||
|
|
||||||
case Participations.leave(event, actor, %{local: false, cancellation_token: token}) do
|
case Participations.leave(event, actor, %{local: false, cancellation_token: token}) do
|
||||||
{:ok, _activity, %Participant{id: participant_id} = _participant} ->
|
{:ok, _activity, %Participant{id: participant_id} = _participant} ->
|
||||||
{:ok, %{event: %{id: event_id}, actor: %{id: actor_id}, id: participant_id}}
|
{:ok, %Event{} = event} = Events.get_event_with_preload(event_id)
|
||||||
|
%Actor{} = actor = Actors.get_actor_with_preload!(actor_id)
|
||||||
|
{:ok, %{event: event, actor: actor, id: participant_id}}
|
||||||
|
|
||||||
{:error, :is_only_organizer} ->
|
{:error, :is_only_organizer} ->
|
||||||
{:error,
|
{:error,
|
||||||
|
@ -202,8 +204,11 @@ defmodule Mobilizon.GraphQL.Resolvers.Participant do
|
||||||
with {:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id),
|
with {:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id),
|
||||||
{:has_event, {:ok, %Event{} = event}} <-
|
{:has_event, {:ok, %Event{} = event}} <-
|
||||||
{:has_event, Events.get_event_with_preload(event_id)},
|
{:has_event, Events.get_event_with_preload(event_id)},
|
||||||
{:ok, _activity, _participant} <- Participations.leave(event, actor) do
|
{:ok, _activity, %Participant{id: participant_id} = _participant} <-
|
||||||
{:ok, %{event: %{id: event_id}, actor: %{id: actor_id}}}
|
Participations.leave(event, actor) do
|
||||||
|
{:ok, %Event{} = event} = Events.get_event_with_preload(event_id)
|
||||||
|
%Actor{} = actor = Actors.get_actor_with_preload!(actor_id)
|
||||||
|
{:ok, %{event: event, actor: actor, id: participant_id}}
|
||||||
else
|
else
|
||||||
{:has_event, _} ->
|
{:has_event, _} ->
|
||||||
{:error, "Event with this ID #{inspect(event_id)} doesn't exist"}
|
{:error, "Event with this ID #{inspect(event_id)} doesn't exist"}
|
||||||
|
|
|
@ -90,8 +90,8 @@ defmodule Mobilizon.GraphQL.Schema.Events.ParticipantType do
|
||||||
object :deleted_participant do
|
object :deleted_participant do
|
||||||
meta(:authorize, :all)
|
meta(:authorize, :all)
|
||||||
field(:id, :id, description: "The participant ID")
|
field(:id, :id, description: "The participant ID")
|
||||||
field(:event, :deleted_object, description: "The participant's event")
|
field(:event, :event, description: "The participant's event")
|
||||||
field(:actor, :deleted_object, description: "The participant's actor")
|
field(:actor, :actor, description: "The participant's actor")
|
||||||
end
|
end
|
||||||
|
|
||||||
object :participant_mutations do
|
object :participant_mutations do
|
||||||
|
|
|
@ -26,6 +26,8 @@ defmodule Mobilizon.Web.PageController do
|
||||||
defdelegate moderation_report(conn, params), to: PageController, as: :index
|
defdelegate moderation_report(conn, params), to: PageController, as: :index
|
||||||
@spec participation_email_confirmation(Plug.Conn.t(), any) :: Plug.Conn.t()
|
@spec participation_email_confirmation(Plug.Conn.t(), any) :: Plug.Conn.t()
|
||||||
defdelegate participation_email_confirmation(conn, params), to: PageController, as: :index
|
defdelegate participation_email_confirmation(conn, params), to: PageController, as: :index
|
||||||
|
@spec participation_email_cancellation(Plug.Conn.t(), any) :: Plug.Conn.t()
|
||||||
|
defdelegate participation_email_cancellation(conn, params), to: PageController, as: :index
|
||||||
@spec user_email_validation(Plug.Conn.t(), any) :: Plug.Conn.t()
|
@spec user_email_validation(Plug.Conn.t(), any) :: Plug.Conn.t()
|
||||||
defdelegate user_email_validation(conn, params), to: PageController, as: :index
|
defdelegate user_email_validation(conn, params), to: PageController, as: :index
|
||||||
@spec my_groups(Plug.Conn.t(), any) :: Plug.Conn.t()
|
@spec my_groups(Plug.Conn.t(), any) :: Plug.Conn.t()
|
||||||
|
|
|
@ -99,6 +99,7 @@ defmodule Mobilizon.Web.Email.Participation do
|
||||||
locale: locale,
|
locale: locale,
|
||||||
event: event,
|
event: event,
|
||||||
jsonLDMetadata: json_ld(participant),
|
jsonLDMetadata: json_ld(participant),
|
||||||
|
participant: participant,
|
||||||
subject: subject
|
subject: subject
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
@ -123,6 +124,7 @@ defmodule Mobilizon.Web.Email.Participation do
|
||||||
locale: locale,
|
locale: locale,
|
||||||
event: event,
|
event: event,
|
||||||
jsonLDMetadata: json_ld(participant),
|
jsonLDMetadata: json_ld(participant),
|
||||||
|
participant: participant,
|
||||||
subject: subject
|
subject: subject
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
|
@ -195,6 +195,12 @@ defmodule Mobilizon.Web.Router do
|
||||||
|
|
||||||
get("/participation/email/confirm/:token", PageController, :participation_email_confirmation)
|
get("/participation/email/confirm/:token", PageController, :participation_email_confirmation)
|
||||||
|
|
||||||
|
get(
|
||||||
|
"/participation/email/cancel/:uuid/:token",
|
||||||
|
PageController,
|
||||||
|
:participation_email_cancellation
|
||||||
|
)
|
||||||
|
|
||||||
get("/validate/email/:token", PageController, :user_email_validation)
|
get("/validate/email/:token", PageController, :user_email_validation)
|
||||||
|
|
||||||
get("/groups/me", PageController, :my_groups)
|
get("/groups/me", PageController, :my_groups)
|
||||||
|
|
|
@ -87,14 +87,39 @@
|
||||||
style="padding: 20px 30px 40px 30px; color: #474467; font-family: 'Roboto', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;"
|
style="padding: 20px 30px 40px 30px; color: #474467; font-family: 'Roboto', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;"
|
||||||
>
|
>
|
||||||
<p style="margin: 0">
|
<p style="margin: 0">
|
||||||
<%= ngettext(
|
<%= gettext(
|
||||||
"Would you wish to cancel your attendance, visit the event page through the link above and click the « Attending » button.",
|
"If you wish to cancel your participation, simply click on the link below."
|
||||||
"Would you wish to cancel your attendance to one or several events, visit the event pages through the links above and click the « Attending » button.",
|
|
||||||
1
|
|
||||||
) %>
|
) %>
|
||||||
</p>
|
</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<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;">
|
||||||
|
<a
|
||||||
|
href={
|
||||||
|
~p"/participation/email/cancel/#{@event.uuid}/#{@participant.metadata.cancellation_token}"
|
||||||
|
|> url()
|
||||||
|
|> URI.decode()
|
||||||
|
}
|
||||||
|
target="_blank"
|
||||||
|
style="font-size: 20px; font-family: 'Roboto', Helvetica, Arial, sans-serif; color: #3C376E; text-decoration: none; padding: 15px 25px; border-radius: 2px; border: 1px solid #3C376E; display: inline-block;"
|
||||||
|
>
|
||||||
|
<%= gettext("Cancel my attendance") %>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td
|
<td
|
||||||
bgcolor="#ffffff"
|
bgcolor="#ffffff"
|
||||||
|
|
|
@ -88,19 +88,62 @@
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<%= if @participant.metadata.cancellation_token do %>
|
||||||
<td
|
<tr>
|
||||||
bgcolor="#ffffff"
|
<td
|
||||||
align="left"
|
bgcolor="#ffffff"
|
||||||
style="padding: 20px 30px 40px 30px; color: #474467; font-family: 'Roboto', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;"
|
align="left"
|
||||||
>
|
style="padding: 20px 30px 40px 30px; color: #474467; font-family: 'Roboto', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;"
|
||||||
<p style="margin: 0">
|
>
|
||||||
<%= gettext(
|
<p style="margin: 0">
|
||||||
"Would you wish to update or cancel your attendance, simply access the event page through the link above and click on the Attending button."
|
<%= gettext(
|
||||||
) %>
|
"If you wish to cancel your participation, simply click on the link below."
|
||||||
</p>
|
) %>
|
||||||
</td>
|
</p>
|
||||||
</tr>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<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;">
|
||||||
|
<a
|
||||||
|
href={
|
||||||
|
~p"/participation/email/cancel/#{@event.uuid}/#{@participant.metadata.cancellation_token}"
|
||||||
|
|> url()
|
||||||
|
|> URI.decode()
|
||||||
|
}
|
||||||
|
target="_blank"
|
||||||
|
style="font-size: 20px; font-family: 'Roboto', Helvetica, Arial, sans-serif; color: #3C376E; text-decoration: none; padding: 15px 25px; border-radius: 2px; border: 1px solid #3C376E; display: inline-block;"
|
||||||
|
>
|
||||||
|
<%= gettext("Cancel my attendance") %>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% else %>
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
bgcolor="#ffffff"
|
||||||
|
align="left"
|
||||||
|
style="padding: 20px 30px 40px 30px; color: #474467; font-family: 'Roboto', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;"
|
||||||
|
>
|
||||||
|
<p style="margin: 0">
|
||||||
|
<%= gettext(
|
||||||
|
"Would you wish to update or cancel your attendance, simply access the event page through the link above and click on the Attending button."
|
||||||
|
) %>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
</table>
|
</table>
|
||||||
<!--[if (gte mso 9)|(IE)]>
|
<!--[if (gte mso 9)|(IE)]>
|
||||||
</td>
|
</td>
|
||||||
|
|
107
src/components/Participation/CancelParticipation.vue
Normal file
107
src/components/Participation/CancelParticipation.vue
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
<template>
|
||||||
|
<section class="container mx-auto">
|
||||||
|
<h1 class="title" v-if="loading">
|
||||||
|
{{ t("Your participation is being cancelled") }}
|
||||||
|
</h1>
|
||||||
|
<div v-else>
|
||||||
|
<div v-if="failed && !leftEvent">
|
||||||
|
<o-notification
|
||||||
|
:title="t('Error while cancelling your participation')"
|
||||||
|
variant="danger"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
t(
|
||||||
|
"Either your participation has already been cancelled, either the validation token is incorrect."
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</o-notification>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="leftEvent">
|
||||||
|
<h1 class="title">
|
||||||
|
{{ t("Your participation has been cancelled") }}
|
||||||
|
</h1>
|
||||||
|
<div class="columns has-text-centered">
|
||||||
|
<div class="column">
|
||||||
|
<o-button
|
||||||
|
tag="router-link"
|
||||||
|
variant="primary"
|
||||||
|
size="large"
|
||||||
|
:to="{
|
||||||
|
name: RouteName.EVENT,
|
||||||
|
params: { uuid: leftEvent.uuid },
|
||||||
|
}"
|
||||||
|
>{{ t("Return to the event page") }}</o-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import RouteName from "../../router/name";
|
||||||
|
import { LEAVE_EVENT } from "../../graphql/event";
|
||||||
|
import { computed, ref, watchEffect } from "vue";
|
||||||
|
import { useMutation } from "@vue/apollo-composable";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
import { useHead } from "@unhead/vue";
|
||||||
|
import { IActor } from "@/types/actor";
|
||||||
|
import { IEvent } from "@/types/event.model";
|
||||||
|
import { useAnonymousActorId } from "@/composition/apollo/config";
|
||||||
|
import { useFetchEventBasic } from "@/composition/apollo/event";
|
||||||
|
|
||||||
|
const { t } = useI18n({ useScope: "global" });
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
title: computed(() => t("Confirm participation")),
|
||||||
|
});
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
uuid: string;
|
||||||
|
token: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const participationToken = computed(() => props.token);
|
||||||
|
const eventUUID = computed(() => props.uuid);
|
||||||
|
|
||||||
|
const loading = ref(true);
|
||||||
|
const failed = ref(false);
|
||||||
|
const leftEvent = ref<IEvent | null>(null);
|
||||||
|
const { anonymousActorId } = useAnonymousActorId();
|
||||||
|
|
||||||
|
const { event } = useFetchEventBasic(eventUUID);
|
||||||
|
|
||||||
|
const { onDone, onError, mutate } = useMutation<{
|
||||||
|
leaveEvent: {
|
||||||
|
id: string;
|
||||||
|
actor: IActor;
|
||||||
|
event: IEvent;
|
||||||
|
};
|
||||||
|
}>(LEAVE_EVENT);
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
if (participationToken.value && event.value) {
|
||||||
|
mutate({
|
||||||
|
token: participationToken.value,
|
||||||
|
eventId: event.value.id,
|
||||||
|
actorId: anonymousActorId.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onDone(async ({ data }) => {
|
||||||
|
if (data?.leaveEvent.event) {
|
||||||
|
leftEvent.value = data?.leaveEvent.event;
|
||||||
|
} else {
|
||||||
|
failed.value = true;
|
||||||
|
}
|
||||||
|
loading.value = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
onError((err) => {
|
||||||
|
console.error(err);
|
||||||
|
failed.value = true;
|
||||||
|
loading.value = false;
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -322,9 +322,14 @@ export const JOIN_EVENT = gql`
|
||||||
export const LEAVE_EVENT = gql`
|
export const LEAVE_EVENT = gql`
|
||||||
mutation LeaveEvent($eventId: ID!, $actorId: ID!, $token: String) {
|
mutation LeaveEvent($eventId: ID!, $actorId: ID!, $token: String) {
|
||||||
leaveEvent(eventId: $eventId, actorId: $actorId, token: $token) {
|
leaveEvent(eventId: $eventId, actorId: $actorId, token: $token) {
|
||||||
|
id
|
||||||
actor {
|
actor {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
|
event {
|
||||||
|
id
|
||||||
|
uuid
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -1630,5 +1630,11 @@
|
||||||
"This announcement will be send to all participants with the statuses selected below. They will not be allowed to reply to your announcement, but they can create a new conversation with you.": "This announcement will be send to all participants with the statuses selected below. They will not be allowed to reply to your announcement, but they can create a new conversation with you.",
|
"This announcement will be send to all participants with the statuses selected below. They will not be allowed to reply to your announcement, but they can create a new conversation with you.": "This announcement will be send to all participants with the statuses selected below. They will not be allowed to reply to your announcement, but they can create a new conversation with you.",
|
||||||
"The following participants are groups, which means group members are able to reply to this conversation:": "The following participants are groups, which means group members are able to reply to this conversation:",
|
"The following participants are groups, which means group members are able to reply to this conversation:": "The following participants are groups, which means group members are able to reply to this conversation:",
|
||||||
"Sent to {count} participants": "Sent to no participants|Sent to one participant|Sent to {count} participants",
|
"Sent to {count} participants": "Sent to no participants|Sent to one participant|Sent to {count} participants",
|
||||||
"There's no announcements yet": "There's no announcements yet"
|
"There's no announcements yet": "There's no announcements yet",
|
||||||
|
"Your participation is being cancelled": "Your participation is being cancelled",
|
||||||
|
"Error while cancelling your participation": "Error while cancelling your participation",
|
||||||
|
"Either your participation has already been cancelled, either the validation token is incorrect.": "Either your participation has already been cancelled, either the validation token is incorrect.",
|
||||||
|
"Your participation has been cancelled": "Your participation has been cancelled",
|
||||||
|
"Return to the event page": "Return to the event page",
|
||||||
|
"Cancel participation": "Cancel participation"
|
||||||
}
|
}
|
|
@ -1624,5 +1624,11 @@
|
||||||
"{user}'s follow request was accepted": "La demande de suivi de {user} a été acceptée",
|
"{user}'s follow request was accepted": "La demande de suivi de {user} a été acceptée",
|
||||||
"{user}'s follow request was rejected": "La demande de suivi de {user} a été rejetée",
|
"{user}'s follow request was rejected": "La demande de suivi de {user} a été rejetée",
|
||||||
"© The OpenStreetMap Contributors": "© Les Contributeur·ices OpenStreetMap",
|
"© The OpenStreetMap Contributors": "© Les Contributeur·ices OpenStreetMap",
|
||||||
"There's no announcements yet": "Il n'y a encore aucune annonce"
|
"There's no announcements yet": "Il n'y a encore aucune annonce",
|
||||||
|
"Your participation is being cancelled": "Votre participation est en cours d'annulation",
|
||||||
|
"Error while cancelling your participation": "Erreur lors de l'annulation de votre participation",
|
||||||
|
"Either your participation has already been cancelled, either the validation token is incorrect.": "Soit votre participation a déjà été annulée, soit le jeton de validation est incorrect.",
|
||||||
|
"Your participation has been cancelled": "Votre participation a été annulée",
|
||||||
|
"Return to the event page": "Retourner à la page de l'événement",
|
||||||
|
"Cancel participation": "Annuler la participation"
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ export enum EventRouteName {
|
||||||
EVENT_PARTICIPATE_WITHOUT_ACCOUNT = "EVENT_PARTICIPATE_WITHOUT_ACCOUNT",
|
EVENT_PARTICIPATE_WITHOUT_ACCOUNT = "EVENT_PARTICIPATE_WITHOUT_ACCOUNT",
|
||||||
EVENT_PARTICIPATE_LOGGED_OUT = "EVENT_PARTICIPATE_LOGGED_OUT",
|
EVENT_PARTICIPATE_LOGGED_OUT = "EVENT_PARTICIPATE_LOGGED_OUT",
|
||||||
EVENT_PARTICIPATE_CONFIRM = "EVENT_PARTICIPATE_CONFIRM",
|
EVENT_PARTICIPATE_CONFIRM = "EVENT_PARTICIPATE_CONFIRM",
|
||||||
|
EVENT_PARTICIPATE_CANCEL = "EVENT_PARTICIPATE_CANCEL",
|
||||||
TAG = "Tag",
|
TAG = "Tag",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,6 +125,18 @@ export const eventRoutes: RouteRecordRaw[] = [
|
||||||
},
|
},
|
||||||
props: true,
|
props: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/participation/email/cancel/:uuid/:token",
|
||||||
|
name: EventRouteName.EVENT_PARTICIPATE_CANCEL,
|
||||||
|
component: () =>
|
||||||
|
import("../components/Participation/CancelParticipation.vue"),
|
||||||
|
meta: {
|
||||||
|
announcer: {
|
||||||
|
message: (): string => t("Cancel participation") as string,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
props: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/tag/:tag",
|
path: "/tag/:tag",
|
||||||
name: EventRouteName.TAG,
|
name: EventRouteName.TAG,
|
||||||
|
|
Loading…
Reference in a new issue