From a877e4d7d928979fb8c628b32ebaff580a7adacc Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Thu, 11 Apr 2019 18:25:32 +0200 Subject: [PATCH] Implement related events Signed-off-by: Thomas Citharel --- lib/mobilizon/events/events.ex | 50 +++++++++++++++++ lib/mobilizon_web/resolvers/event.ex | 46 ++++++++++++++++ lib/mobilizon_web/schema/event.ex | 5 ++ ...190411161557_event_event_tag_on_delete.exs | 23 ++++++++ test/mobilizon/events/events_test.exs | 2 +- .../resolvers/event_resolver_test.exs | 54 +++++++++++++++++++ test/support/factory.ex | 3 +- 7 files changed, 181 insertions(+), 2 deletions(-) create mode 100644 priv/repo/migrations/20190411161557_event_event_tag_on_delete.exs diff --git a/lib/mobilizon/events/events.ex b/lib/mobilizon/events/events.ex index 42ae4b484..8c7c48079 100644 --- a/lib/mobilizon/events/events.ex +++ b/lib/mobilizon/events/events.ex @@ -45,6 +45,34 @@ defmodule Mobilizon.Events do {:ok, events, count_events} end + @doc """ + Get an actor's eventual upcoming public event + """ + @spec get_actor_upcoming_public_event(Actor.t(), String.t()) :: Event.t() | nil + def get_actor_upcoming_public_event(%Actor{id: actor_id} = _actor, not_event_uuid \\ nil) do + query = + from( + e in Event, + where: + e.organizer_actor_id == ^actor_id and e.visibility in [^:public, ^:unlisted] and + e.begins_on > ^DateTime.utc_now(), + order_by: [asc: :begins_on], + preload: [ + :organizer_actor, + :tags, + :participants, + :physical_address + ] + ) + + query = + if is_nil(not_event_uuid), + do: query, + else: from(q in query, where: q.uuid != ^not_event_uuid) + + Repo.one(query) + end + def count_local_events do Repo.one( from( @@ -274,6 +302,28 @@ defmodule Mobilizon.Events do Repo.all(query) end + @doc """ + Find events with the same tags + """ + @spec find_similar_events_by_common_tags(list(), integer()) :: {:ok, list(Event.t())} + def find_similar_events_by_common_tags(tags, limit \\ 2) do + tags_ids = Enum.map(tags, & &1.id) + + query = + from(e in Event, + distinct: e.uuid, + join: te in "events_tags", + on: e.id == te.event_id, + where: e.begins_on > ^DateTime.utc_now(), + where: e.visibility in [^:public, ^:unlisted], + where: te.tag_id in ^tags_ids, + order_by: [asc: e.begins_on], + limit: ^limit + ) + + Repo.all(query) + end + @doc """ Creates a event. diff --git a/lib/mobilizon_web/resolvers/event.ex b/lib/mobilizon_web/resolvers/event.ex index ac11e1b12..5ae1710fd 100644 --- a/lib/mobilizon_web/resolvers/event.ex +++ b/lib/mobilizon_web/resolvers/event.ex @@ -3,12 +3,14 @@ defmodule MobilizonWeb.Resolvers.Event do Handles the event-related GraphQL calls """ alias Mobilizon.Activity + alias Mobilizon.Events alias Mobilizon.Events.{Event, Participant} alias Mobilizon.Actors.Actor alias Mobilizon.Users.User # We limit the max number of events that can be retrieved @event_max_limit 100 + @number_of_related_events 3 def list_events(_parent, %{page: page, limit: limit}, _resolution) when limit < @event_max_limit do @@ -43,6 +45,50 @@ defmodule MobilizonWeb.Resolvers.Event do {:ok, Mobilizon.Events.list_participants_for_event(uuid, 1, 10)} end + @doc """ + List related events + """ + def list_related_events( + %Event{tags: tags, organizer_actor: organizer_actor}, + _args, + _resolution + ) do + # We get the organizer's next public event + events = + [Events.get_actor_upcoming_public_event(organizer_actor, uuid)] |> Enum.filter(&is_map/1) + + # uniq_by : It's possible event_from_same_actor is inside events_from_tags + events = + (events ++ + Events.find_similar_events_by_common_tags( + tags, + @number_of_related_events - length(events) + )) + |> uniq_events() + + # TODO: We should use tag_relations to find more appropriate events + + # We've considered all recommended events, so we fetch the latest events + events = + if @number_of_related_events - length(events) > 0 do + (events ++ + Events.list_events(1, @number_of_related_events, :begins_on, :asc, true, true)) + |> uniq_events() + else + events + end + + events = + events + # We remove the same event from the results + |> Enum.filter(fn event -> event.uuid != uuid end) + # We return only @number_of_related_events right now + |> Enum.take(@number_of_related_events) + + # TODO: We should use tag_relations to find more events + {:ok, events} + end + @doc """ Join an event for an actor """ diff --git a/lib/mobilizon_web/schema/event.ex b/lib/mobilizon_web/schema/event.ex index c4bcb1c6c..ac72f93f2 100644 --- a/lib/mobilizon_web/schema/event.ex +++ b/lib/mobilizon_web/schema/event.ex @@ -56,6 +56,11 @@ defmodule MobilizonWeb.Schema.EventType do description: "The event's participants" ) + field(:related_events, list_of(:event), + resolve: &MobilizonWeb.Resolvers.Event.list_related_events/3, + description: "Events related to this one" + ) + # field(:tracks, list_of(:track)) # field(:sessions, list_of(:session)) diff --git a/priv/repo/migrations/20190411161557_event_event_tag_on_delete.exs b/priv/repo/migrations/20190411161557_event_event_tag_on_delete.exs new file mode 100644 index 000000000..2ddffb82a --- /dev/null +++ b/priv/repo/migrations/20190411161557_event_event_tag_on_delete.exs @@ -0,0 +1,23 @@ +defmodule Mobilizon.Repo.Migrations.EventEventTagOnDelete do + use Ecto.Migration + + def up do + drop(constraint(:events_tags, "events_tags_event_id_fkey")) + drop(constraint(:events_tags, "events_tags_tag_id_fkey")) + + alter table(:events_tags) do + modify(:event_id, references(:events, on_delete: :delete_all)) + modify(:tag_id, references(:tags, on_delete: :delete_all)) + end + end + + def down do + drop(constraint(:events_tags, "events_tags_event_id_fkey")) + drop(constraint(:events_tags, "events_tags_tag_id_fkey")) + + alter table(:events_tags) do + modify(:event_id, references(:events)) + modify(:tag_id, references(:tags)) + end + end +end diff --git a/test/mobilizon/events/events_test.exs b/test/mobilizon/events/events_test.exs index b07283994..77a0ad0c9 100644 --- a/test/mobilizon/events/events_test.exs +++ b/test/mobilizon/events/events_test.exs @@ -20,7 +20,7 @@ defmodule Mobilizon.EventsTest do setup do actor = insert(:actor) - event = insert(:event, organizer_actor: actor) + event = insert(:event, organizer_actor: actor, visibility: :public) {:ok, actor: actor, event: event} end diff --git a/test/mobilizon_web/resolvers/event_resolver_test.exs b/test/mobilizon_web/resolvers/event_resolver_test.exs index 636386364..16473ff64 100644 --- a/test/mobilizon_web/resolvers/event_resolver_test.exs +++ b/test/mobilizon_web/resolvers/event_resolver_test.exs @@ -322,5 +322,59 @@ defmodule MobilizonWeb.Resolvers.EventResolverTest do assert hd(json_response(res, 200)["errors"])["message"] =~ "cannot delete" end + + test "list_related_events/3 should give related events", %{ + conn: conn, + actor: actor + } do + tag1 = insert(:tag, title: "Elixir", slug: "elixir") + tag2 = insert(:tag, title: "PostgreSQL", slug: "postgresql") + + event = insert(:event, title: "Initial event", organizer_actor: actor, tags: [tag1, tag2]) + + event2 = + insert(:event, + title: "Event from same actor", + organizer_actor: actor, + visibility: :public, + begins_on: Timex.shift(DateTime.utc_now(), days: 3) + ) + + event3 = + insert(:event, + title: "Event with same tags", + tags: [tag1, tag2], + visibility: :public, + begins_on: Timex.shift(DateTime.utc_now(), days: 3) + ) + + query = """ + { + event(uuid: "#{event.uuid}") { + uuid, + title, + tags { + id + }, + related_events { + uuid, + title, + tags { + id + } + } + } + } + """ + + res = + conn + |> get("/api", AbsintheHelpers.query_skeleton(query, "event")) + + assert hd(json_response(res, 200)["data"]["event"]["related_events"])["uuid"] == event2.uuid + + assert hd(tl(json_response(res, 200)["data"]["event"]["related_events"]))["uuid"] == + event3.uuid + end end end diff --git a/test/support/factory.ex b/test/support/factory.ex index 324ec689f..a94e271ef 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -95,7 +95,7 @@ defmodule Mobilizon.Factory do def event_factory do actor = build(:actor) - start = Timex.now() + start = Timex.shift(DateTime.utc_now(), hours: 2) uuid = Ecto.UUID.generate() %Mobilizon.Events.Event{ @@ -108,6 +108,7 @@ defmodule Mobilizon.Factory do category: sequence("something"), physical_address: build(:address), visibility: :public, + tags: build_list(3, :tag), url: "#{actor.url}/#{uuid}", uuid: uuid }