From bf25d227861c2f6e5914379f981f32436372807d Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Fri, 11 Oct 2019 11:50:06 +0200 Subject: [PATCH 1/4] Make sure people can't join an event with limited participants Signed-off-by: Thomas Citharel --- lib/mobilizon/events/events.ex | 16 +++++ lib/mobilizon_web/resolvers/event.ex | 3 + lib/service/activity_pub/activity_pub.ex | 12 +++- .../resolvers/participant_resolver_test.exs | 70 +++++++++++++++++++ test/support/factory.ex | 3 +- 5 files changed, 101 insertions(+), 3 deletions(-) diff --git a/lib/mobilizon/events/events.ex b/lib/mobilizon/events/events.ex index 3bae79e1a..739c65a41 100644 --- a/lib/mobilizon/events/events.ex +++ b/lib/mobilizon/events/events.ex @@ -762,6 +762,17 @@ defmodule Mobilizon.Events do |> Repo.aggregate(:count, :id) end + @doc """ + Counts participant participants. + """ + @spec count_participant_participants(integer | String.t()) :: integer + def count_participant_participants(event_id) do + event_id + |> count_participants_query() + |> filter_participant_role() + |> Repo.aggregate(:count, :id) + end + @doc """ Counts unapproved participants. """ @@ -1457,6 +1468,11 @@ defmodule Mobilizon.Events do from(p in query, where: p.role not in ^[:not_approved, :rejected]) end + @spec filter_participant_role(Ecto.Query.t()) :: Ecto.Query.t() + defp filter_participant_role(query) do + from(p in query, where: p.role == ^:participant) + end + @spec filter_unapproved_role(Ecto.Query.t()) :: Ecto.Query.t() defp filter_unapproved_role(query) do from(p in query, where: p.role == ^:not_approved) diff --git a/lib/mobilizon_web/resolvers/event.ex b/lib/mobilizon_web/resolvers/event.ex index 9fb155bf3..7105e628f 100644 --- a/lib/mobilizon_web/resolvers/event.ex +++ b/lib/mobilizon_web/resolvers/event.ex @@ -170,6 +170,9 @@ defmodule MobilizonWeb.Resolvers.Event do |> Map.put(:actor, Person.proxify_pictures(actor)) do {:ok, participant} else + {:maximum_attendee_capacity, _} -> + {:error, "The event has already reached it's maximum capacity"} + {:has_event, _} -> {:error, "Event with this ID #{inspect(event_id)} doesn't exist"} diff --git a/lib/service/activity_pub/activity_pub.ex b/lib/service/activity_pub/activity_pub.ex index b98753c06..f3de44b05 100644 --- a/lib/service/activity_pub/activity_pub.ex +++ b/lib/service/activity_pub/activity_pub.ex @@ -411,8 +411,16 @@ defmodule Mobilizon.Service.ActivityPub do def join(object, actor, local \\ true) - def join(%Event{} = event, %Actor{} = actor, local) do - with role <- Mobilizon.Events.get_default_participant_role(event), + def join(%Event{options: options} = event, %Actor{} = actor, local) do + # TODO Refactor me for federation + with maximum_attendee_capacity <- + Map.get(options, :maximum_attendee_capacity, 2_000_000) || false, + {:maximum_attendee_capacity, true} <- + {:maximum_attendee_capacity, + !maximum_attendee_capacity || + Mobilizon.Events.count_participant_participants(event.id) < + maximum_attendee_capacity}, + role <- Mobilizon.Events.get_default_participant_role(event), {:ok, %Participant{} = participant} <- Mobilizon.Events.create_participant(%{ role: role, diff --git a/test/mobilizon_web/resolvers/participant_resolver_test.exs b/test/mobilizon_web/resolvers/participant_resolver_test.exs index c0e69ea2f..bd195d47c 100644 --- a/test/mobilizon_web/resolvers/participant_resolver_test.exs +++ b/test/mobilizon_web/resolvers/participant_resolver_test.exs @@ -74,6 +74,76 @@ defmodule MobilizonWeb.Resolvers.ParticipantResolverTest do assert hd(json_response(res, 200)["errors"])["message"] =~ "already a participant" end + test "actor_join_event/3 doesn't work if the event already has too much participants", %{ + conn: conn, + actor: actor + } do + event = insert(:event, options: %{maximum_attendee_capacity: 2}) + insert(:participant, event: event, actor: actor, role: :creator) + insert(:participant, event: event, role: :participant) + insert(:participant, event: event, role: :not_approved) + insert(:participant, event: event, role: :rejected) + user_participant = insert(:user) + actor_participant = insert(:actor, user: user_participant) + + mutation = """ + mutation { + joinEvent( + actor_id: #{actor_participant.id}, + event_id: #{event.id} + ) { + role, + actor { + id + }, + event { + id + } + } + } + """ + + res = + conn + |> auth_conn(user_participant) + |> post("/api", AbsintheHelpers.mutation_skeleton(mutation)) + + assert json_response(res, 200)["errors"] == nil + assert json_response(res, 200)["data"]["joinEvent"]["role"] == "PARTICIPANT" + assert json_response(res, 200)["data"]["joinEvent"]["event"]["id"] == to_string(event.id) + + assert json_response(res, 200)["data"]["joinEvent"]["actor"]["id"] == + to_string(actor_participant.id) + + user_participant_2 = insert(:user) + actor_participant_2 = insert(:actor, user: user_participant_2) + + mutation = """ + mutation { + joinEvent( + actor_id: #{actor_participant_2.id}, + event_id: #{event.id} + ) { + role, + actor { + id + }, + event { + id + } + } + } + """ + + res = + conn + |> auth_conn(user_participant_2) + |> post("/api", AbsintheHelpers.mutation_skeleton(mutation)) + + assert hd(json_response(res, 200)["errors"])["message"] == + "The event has already reached it's maximum capacity" + end + test "actor_join_event/3 should check the actor is owned by the user", %{ conn: conn, user: user diff --git a/test/support/factory.ex b/test/support/factory.ex index c866ca6c4..7e7c8597f 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -124,7 +124,8 @@ defmodule Mobilizon.Factory do url: Routes.page_url(Endpoint, :event, uuid), picture: insert(:picture), uuid: uuid, - join_options: :free + join_options: :free, + options: %{} } end From 98472e7222e2b6cdbef1c5e4fd1d1538f16a224e Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Fri, 11 Oct 2019 11:54:57 +0200 Subject: [PATCH 2/4] Expose participants number through API Signed-off-by: Thomas Citharel --- lib/mobilizon_web/resolvers/event.ex | 3 ++- lib/mobilizon_web/schema/event.ex | 1 + schema.graphql | 5 ++++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/mobilizon_web/resolvers/event.ex b/lib/mobilizon_web/resolvers/event.ex index 7105e628f..a19c6eb1d 100644 --- a/lib/mobilizon_web/resolvers/event.ex +++ b/lib/mobilizon_web/resolvers/event.ex @@ -101,7 +101,8 @@ defmodule MobilizonWeb.Resolvers.Event do %{ approved: Mobilizon.Events.count_approved_participants(id), unapproved: Mobilizon.Events.count_unapproved_participants(id), - rejected: Mobilizon.Events.count_rejected_participants(id) + rejected: Mobilizon.Events.count_rejected_participants(id), + participants: Mobilizon.Events.count_participant_participants(id), }} end diff --git a/lib/mobilizon_web/schema/event.ex b/lib/mobilizon_web/schema/event.ex index d6cc0b584..b0bc36a3f 100644 --- a/lib/mobilizon_web/schema/event.ex +++ b/lib/mobilizon_web/schema/event.ex @@ -115,6 +115,7 @@ defmodule MobilizonWeb.Schema.EventType 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") + field(:participants, :integer, description: "The number of simple participants (excluding creators)") end object :event_offer do diff --git a/schema.graphql b/schema.graphql index 036094520..9cacfb2e7 100644 --- a/schema.graphql +++ b/schema.graphql @@ -1,5 +1,5 @@ # source: http://localhost:4000/api -# timestamp: Fri Oct 04 2019 15:04:46 GMT+0200 (GMT+02:00) +# timestamp: Fri Oct 11 2019 11:53:52 GMT+0200 (Central European Summer Time) schema { query: RootQueryType @@ -681,6 +681,9 @@ type ParticipantStats { """The number of approved participants""" approved: Int + """The number of simple participants (excluding creators)""" + participants: Int + """The number of rejected participants""" rejected: Int From 4499fb2f311e8c075dc0784a4abfcc278c44b9e5 Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Fri, 11 Oct 2019 15:06:58 +0200 Subject: [PATCH 3/4] Fix participation section, show how many places are available Signed-off-by: Thomas Citharel --- js/src/graphql/event.ts | 12 ++- js/src/i18n/en_US.json | 3 +- js/src/i18n/fr_FR.json | 3 +- js/src/types/event.model.ts | 3 +- js/src/views/Event/Event.vue | 140 +++++++++++++++++++-------- lib/mobilizon_web/resolvers/event.ex | 2 +- lib/mobilizon_web/schema/event.ex | 5 +- 7 files changed, 118 insertions(+), 50 deletions(-) diff --git a/js/src/graphql/event.ts b/js/src/graphql/event.ts index 1ce5d1b24..92edeeb36 100644 --- a/js/src/graphql/event.ts +++ b/js/src/graphql/event.ts @@ -101,7 +101,8 @@ export const FETCH_EVENT = gql` # }, participantStats { approved, - unapproved + unapproved, + participants }, tags { ${tagsQuery} @@ -257,7 +258,8 @@ export const CREATE_EVENT = gql` }, participantStats { approved, - unapproved + unapproved, + participants }, tags { ${tagsQuery} @@ -341,7 +343,8 @@ export const EDIT_EVENT = gql` }, participantStats { approved, - unapproved + unapproved, + participants }, tags { ${tagsQuery} @@ -407,7 +410,8 @@ export const PARTICIPANTS = gql` participantStats { approved, unapproved, - rejected + rejected, + participants } } } diff --git a/js/src/i18n/en_US.json b/js/src/i18n/en_US.json index 8d19829b6..29ee401ae 100644 --- a/js/src/i18n/en_US.json +++ b/js/src/i18n/en_US.json @@ -273,5 +273,6 @@ "{count} participants": "{count} participants", "{count} requests waiting": "{count} requests waiting", "{license} guarantees {respect} of the people who will use it. Since {source}, anyone can audit it, which guarantees its transparency.": "{license} guarantees {respect} of the people who will use it. Since {source}, anyone can audit it, which guarantees its transparency.", - "© The Mobilizon Contributors {date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks": "© The Mobilizon Contributors {date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks" + "© The Mobilizon Contributors {date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks": "© The Mobilizon Contributors {date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks", + "All the places have already been taken": "All the places have been taken|One place is still available|{places} places are still available" } \ No newline at end of file diff --git a/js/src/i18n/fr_FR.json b/js/src/i18n/fr_FR.json index dbed4d118..9a58e5ff6 100644 --- a/js/src/i18n/fr_FR.json +++ b/js/src/i18n/fr_FR.json @@ -312,5 +312,6 @@ "{count} participants": "Un⋅e participant⋅e|{count} participant⋅e⋅s", "{count} requests waiting": "Un⋅e demande en attente|{count} demandes en attente", "{license} guarantees {respect} of the people who will use it. Since {source}, anyone can audit it, which guarantees its transparency.": "{license} garantit {respect} des personnes qui l'utiliseront. Puisque {source}, il est publiquement auditable, ce qui garantit sa transparence.", - "© The Mobilizon Contributors {date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks": "© Les contributeurs de Mobilizon {date} - Fait avec Elixir, Phoenix, VueJS & et de l'amour et des semaines" + "© The Mobilizon Contributors {date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks": "© Les contributeurs de Mobilizon {date} - Fait avec Elixir, Phoenix, VueJS & et de l'amour et des semaines", + "All the places have already been taken": "Toutes les places ont été prises|Une place est encore disponible|{places} places sont encore disponibles" } \ No newline at end of file diff --git a/js/src/types/event.model.ts b/js/src/types/event.model.ts index 82aac6692..66e946a38 100644 --- a/js/src/types/event.model.ts +++ b/js/src/types/event.model.ts @@ -112,6 +112,7 @@ export interface IEvent { approved: number; unapproved: number; rejected: number; + participants: number; }; participants: IParticipant[]; @@ -178,7 +179,7 @@ export class EventModel implements IEvent { publishAt = new Date(); - participantStats = { approved: 0, unapproved: 0, rejected: 0 }; + participantStats = { approved: 0, unapproved: 0, rejected: 0, participants: 0 }; participants: IParticipant[] = []; relatedEvents: IEvent[] = []; diff --git a/js/src/views/Event/Event.vue b/js/src/views/Event/Event.vue index 2ee20b502..b108cee94 100644 --- a/js/src/views/Event/Event.vue +++ b/js/src/views/Event/Event.vue @@ -17,17 +17,24 @@
-

{{ event.title }}

+
+

{{ event.title }}

+ + + {{ $tc('One person is going', event.participantStats.approved, {approved: event.participantStats.approved}) }} + + + {{ $tc('You and one other person are going to this event', event.participantStats.participants, { approved: event.participantStats.participants }) }} + + + {{ $tc('All the places have already been taken', numberOfPlacesStillAvailable, { places: numberOfPlacesStillAvailable}) }} + + +
-
- - {{ $tc('One person is going', event.participantStats.approved, {approved: event.participantStats.approved}) }} - - - {{ $tc('You and one other person are going to this event', event.participantStats.approved - 1, {approved: event.participantStats.approved - 1}) }} - +