From 5ee97dfa4381ada5371e542da006985a4cd80242 Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Thu, 11 Apr 2019 18:25:32 +0200 Subject: [PATCH 1/2] 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 } From f241251b24bafe65c6301f78f899d55b7cd5e2ae Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Fri, 12 Apr 2019 17:00:55 +0200 Subject: [PATCH 2/2] Show related events Signed-off-by: Thomas Citharel --- js/src/graphql/event.ts | 15 +++++++++++ js/src/i18n/locale/en_US/LC_MESSAGES/app.po | 26 +++++++++--------- js/src/i18n/locale/fr_FR/LC_MESSAGES/app.po | 30 ++++++++++----------- js/src/i18n/translations.json | 2 +- js/src/types/event.model.ts | 3 +++ js/src/views/Event/Event.vue | 18 +++++++------ lib/mobilizon/events/events.ex | 29 ++++++++++++++++++-- lib/mobilizon_web/resolvers/event.ex | 8 +++--- 8 files changed, 89 insertions(+), 42 deletions(-) diff --git a/js/src/graphql/event.ts b/js/src/graphql/event.ts index d4c5dafbe..66a1a046a 100644 --- a/js/src/graphql/event.ts +++ b/js/src/graphql/event.ts @@ -43,6 +43,7 @@ export const FETCH_EVENT = gql` organizerActor { avatarUrl, preferredUsername, + domain, name, }, # attributedTo { @@ -56,6 +57,20 @@ export const FETCH_EVENT = gql` tags { slug, title + }, + relatedEvents { + uuid, + title, + beginsOn, + physicalAddress { + description + }, + organizerActor { + avatarUrl, + preferredUsername, + domain, + name, + } } } } diff --git a/js/src/i18n/locale/en_US/LC_MESSAGES/app.po b/js/src/i18n/locale/en_US/LC_MESSAGES/app.po index 58f29ec8f..3a6b4c277 100644 --- a/js/src/i18n/locale/en_US/LC_MESSAGES/app.po +++ b/js/src/i18n/locale/en_US/LC_MESSAGES/app.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: mobilizon 0.1.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2019-04-10 16:31+0200\n" +"POT-Creation-Date: 2019-04-12 16:47+0200\n" "PO-Revision-Date: 2019-04-08 20:58+0200\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -29,7 +29,7 @@ msgstr "A validation email was sent to %{email}" msgid "About" msgstr "About" -#: src/views/Event/Event.vue:138 +#: src/views/Event/Event.vue:137 msgid "About this event" msgstr "About this event" @@ -41,7 +41,7 @@ msgstr "About this instance" msgid "Add a new profile" msgstr "Add a new profile" -#: src/views/Event/Event.vue:44 src/views/Event/Event.vue:217 +#: src/views/Event/Event.vue:44 src/views/Event/Event.vue:216 msgid "Add to my calendar" msgstr "Add to my calendar" @@ -53,7 +53,7 @@ msgstr "Are you going to this event?" msgid "Before you can login, you need to click on the link inside it to validate your account" msgstr "Before you can login, you need to click on the link inside it to validate your account" -#: src/views/Event/Event.vue:101 +#: src/views/Event/Event.vue:100 msgid "By %{ name }" msgstr "By %{ name }" @@ -93,7 +93,7 @@ msgstr "Create your communities and your events" msgid "Current" msgstr "Current" -#: src/views/Account/Profile.vue:93 src/views/Event/Event.vue:64 +#: src/views/Account/Profile.vue:93 src/views/Event/Event.vue:63 msgid "Delete" msgstr "Delete" @@ -101,7 +101,7 @@ msgstr "Delete" msgid "Didn't receive the instructions ?" msgstr "Didn't receive the instructions ?" -#: src/views/Event/Event.vue:59 +#: src/views/Event/Event.vue:58 msgid "Edit" msgstr "Edit" @@ -205,7 +205,7 @@ msgstr "Members" msgid "My account" msgstr "My account" -#: src/views/Event/Event.vue:70 +#: src/views/Event/Event.vue:69 msgid "No address defined" msgstr "No address defined" @@ -304,7 +304,7 @@ msgstr "Reset my password" msgid "RSS/Atom Feed" msgstr "RSS/Atom Feed" -#: src/views/PageNotFound.vue:18 src/components/SearchField.vue:19 +#: src/views/PageNotFound.vue:19 src/components/SearchField.vue:19 msgid "Search" msgstr "Search" @@ -320,11 +320,11 @@ msgstr "Send confirmation email again" msgid "Send email to reset my password" msgstr "Send email to reset my password" -#: src/views/Event/Event.vue:206 +#: src/views/Event/Event.vue:205 msgid "Share this event" msgstr "Share this event" -#: src/views/Event/Event.vue:79 +#: src/views/Event/Event.vue:78 msgid "Show map" msgstr "Show map" @@ -340,7 +340,7 @@ msgstr "The %{ date } at %{ time }" msgid "The %{ date } from %{ startTime } to %{ endTime }" msgstr "The %{ date } from %{ startTime } to %{ endTime }" -#: src/views/Event/Event.vue:141 +#: src/views/Event/Event.vue:140 msgid "The event organizer didn't add any description." msgstr "The event organizer didn't add any description." @@ -348,7 +348,7 @@ msgstr "The event organizer didn't add any description." msgid "The page you're looking for doesn't exist." msgstr "" -#: src/views/Event/Event.vue:224 +#: src/views/Event/Event.vue:223 msgid "These events may interest you" msgstr "These events may interest you" @@ -434,6 +434,6 @@ msgstr "Your local administrator resumed it's policy:" msgid "World map" msgstr "World map" -#: src/views/PageNotFound.vue:40 +#: src/views/PageNotFound.vue:42 msgid "Search events, groups, etc." msgstr "" diff --git a/js/src/i18n/locale/fr_FR/LC_MESSAGES/app.po b/js/src/i18n/locale/fr_FR/LC_MESSAGES/app.po index 92c02bf72..ef0b304f4 100644 --- a/js/src/i18n/locale/fr_FR/LC_MESSAGES/app.po +++ b/js/src/i18n/locale/fr_FR/LC_MESSAGES/app.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: mobilizon 0.1.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2019-04-10 16:31+0200\n" -"PO-Revision-Date: 2019-04-10 16:33+0200\n" +"POT-Creation-Date: 2019-04-12 16:47+0200\n" +"PO-Revision-Date: 2019-04-12 16:45+0200\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: fr_FR\n" @@ -30,7 +30,7 @@ msgstr "Un email de validation a été envoyé à %{email}" msgid "About" msgstr "À propos" -#: src/views/Event/Event.vue:138 +#: src/views/Event/Event.vue:137 msgid "About this event" msgstr "À propos de cet événement" @@ -42,7 +42,7 @@ msgstr "À propos de cette instance" msgid "Add a new profile" msgstr "Ajouter un nouveau profil" -#: src/views/Event/Event.vue:44 src/views/Event/Event.vue:217 +#: src/views/Event/Event.vue:44 src/views/Event/Event.vue:216 msgid "Add to my calendar" msgstr "Ajouter à mon agenda" @@ -54,7 +54,7 @@ msgstr "Allez-vous à cet événement ?" msgid "Before you can login, you need to click on the link inside it to validate your account" msgstr "Avant que vous puissiez vous enregistrer, vous devez cliquer sur le lien à l'intérieur pour valider votre compte" -#: src/views/Event/Event.vue:101 +#: src/views/Event/Event.vue:100 msgid "By %{ name }" msgstr "Par %{name}" @@ -94,7 +94,7 @@ msgstr "Créer vos communautés et vos événements" msgid "Current" msgstr "Actuel" -#: src/views/Account/Profile.vue:93 src/views/Event/Event.vue:64 +#: src/views/Account/Profile.vue:93 src/views/Event/Event.vue:63 msgid "Delete" msgstr "Supprimer" @@ -102,7 +102,7 @@ msgstr "Supprimer" msgid "Didn't receive the instructions ?" msgstr "Vous n'avez pas reçu les instructions ?" -#: src/views/Event/Event.vue:59 +#: src/views/Event/Event.vue:58 msgid "Edit" msgstr "Éditer" @@ -206,7 +206,7 @@ msgstr "Membres" msgid "My account" msgstr "Mon compte" -#: src/views/Event/Event.vue:70 +#: src/views/Event/Event.vue:69 msgid "No address defined" msgstr "Aucune adresse définie" @@ -305,7 +305,7 @@ msgstr "Réinitialiser mon mot de passe" msgid "RSS/Atom Feed" msgstr "Flux RSS/Atom" -#: src/views/PageNotFound.vue:18 src/components/SearchField.vue:19 +#: src/views/PageNotFound.vue:19 src/components/SearchField.vue:19 msgid "Search" msgstr "Rechercher" @@ -321,11 +321,11 @@ msgstr "Envoyer l'email de confirmation à nouveau" msgid "Send email to reset my password" msgstr "Envoyer un email pour réinitialiser mon mot de passe" -#: src/views/Event/Event.vue:206 +#: src/views/Event/Event.vue:205 msgid "Share this event" -msgstr "Partager cet événement" +msgstr "Partager l'événement" -#: src/views/Event/Event.vue:79 +#: src/views/Event/Event.vue:78 msgid "Show map" msgstr "Afficher la carte" @@ -341,7 +341,7 @@ msgstr "Le %{ date } à %{ time }" msgid "The %{ date } from %{ startTime } to %{ endTime }" msgstr "Le %{ date } de %{ startTime } à %{ endTime }" -#: src/views/Event/Event.vue:141 +#: src/views/Event/Event.vue:140 msgid "The event organizer didn't add any description." msgstr "L'organisateur de l'événement n'a pas ajouté de description." @@ -349,7 +349,7 @@ msgstr "L'organisateur de l'événement n'a pas ajouté de description." msgid "The page you're looking for doesn't exist." msgstr "La page que vous recherchez n'existe pas." -#: src/views/Event/Event.vue:224 +#: src/views/Event/Event.vue:223 msgid "These events may interest you" msgstr "Ces événements peuvent vous intéresser" @@ -435,6 +435,6 @@ msgstr "Votre administrateur local a résumé sa politique ainsi :" msgid "World map" msgstr "Carte mondiale" -#: src/views/PageNotFound.vue:40 +#: src/views/PageNotFound.vue:42 msgid "Search events, groups, etc." msgstr "Rechercher des événements, des groupes, etc." diff --git a/js/src/i18n/translations.json b/js/src/i18n/translations.json index 08df4b0df..8e863a82c 100644 --- a/js/src/i18n/translations.json +++ b/js/src/i18n/translations.json @@ -1 +1 @@ -{"en_US":{"© 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","A validation email was sent to %{email}":"A validation email was sent to %{email}","About":"About","About this event":"About this event","About this instance":"About this instance","Add a new profile":"Add a new profile","Add to my calendar":"Add to my calendar","Are you going to this event?":"Are you going to this event?","Before you can login, you need to click on the link inside it to validate your account":"Before you can login, you need to click on the link inside it to validate your account","By %{ name }":"By %{ name }","Create a new event":"Create a new event","Create a new group":"Create a new group","Create group":"Create group","Create my event":"Create my event","Create my group":"Create my group","Create my profile":"Create my profile","Create token":"Create token","Create your communities and your events":"Create your communities and your events","Current":"Current","Delete":"Delete","Didn't receive the instructions ?":"Didn't receive the instructions ?","Edit":"Edit","Either the account is already validated, either the validation token is incorrect.":"Either the account is already validated, either the validation token is incorrect.","Event list":"Event list","Events":"Events","Events nearby you":"Events nearby you","Events you're going at":"Events you're going at","Features":"Features","Forgot your password ?":"Forgot your password ?","From the %{ startDate } at %{ startTime } to the %{ endDate } at %{ endTime }":"From the %{ startDate } at %{ startTime } to the %{ endDate } at %{ endTime }","Group List":"Group List","Groups":"Groups","iCal Feed":"iCal Feed","Identities":"Identities","If an account with this email exists, we just sent another confirmation email to %{email}":"If an account with this email exists, we just sent another confirmation email to %{email}","Join":"Join","Learn more on\n joinmobilizon.org":"Learn more on\n joinmobilizon.org","Leave":"Leave","Legal":"Legal","License":"License","Log in":"Log in","Log out":"Log out","Login":"Login","meditate a bit":"meditate a bit","Members":"Members","My account":"My account","No address defined":"No address defined","No events found":"No events found","No group found":"No group found","No groups found":"No groups found","Organized":"Organized","Organizer":"Organizer","Other stuff…":"Other stuff…","Password reset":"Password reset","Please be nice to each other":"Please be nice to each other","Please check you spam folder if you didn't receive the email.":"Please check you spam folder if you didn't receive the email.","Please read the full rules":"Please read the full rules","Private feeds":"Private feeds","public event":"public event","Public feeds":"Public feeds","Public iCal Feed":"Public iCal Feed","Public RSS/Atom Feed":"Public RSS/Atom Feed","Register":"Register","Register an account on Mobilizon!":"Register an account on Mobilizon!","Registration is currently closed.":"Registration is currently closed.","Resend confirmation email":"Resend confirmation email","Reset my password":"Reset my password","RSS/Atom Feed":"RSS/Atom Feed","Search":"Search","Search results: « %{ search } »":"Search results: « %{ search } »","Send confirmation email again":"Send confirmation email again","Send email to reset my password":"Send email to reset my password","Share this event":"Share this event","Show map":"Show map","Sign up":"Sign up","The %{ date } at %{ time }":"The %{ date } at %{ time }","The %{ date } from %{ startTime } to %{ endTime }":"The %{ date } from %{ startTime } to %{ endTime }","The event organizer didn't add any description.":"The event organizer didn't add any description.","These events may interest you":"These events may interest you","This instance isn't opened to registrations, but you can register on other instances.":"This instance isn't opened to registrations, but you can register on other instances.","Unknown error.":"Unknown error.","User logout":"User logout","We just sent an email to %{email}":"We just sent an email to %{email}","Welcome back %{username}":"Welcome back %{username}","Welcome back!":"Welcome back!","You announced that you're going to this event.":"You announced that you're going to this event.","You are already logged-in.":"You are already logged-in.","You are an organizer.":"You are an organizer.","You have one event in %{ days } days.":["You have one event in %{ days } days.","You have %{ count } events in %{ days } days"],"You have one event today.":["You have one event today.","You have %{ count } events today"],"You have one event tomorrow.":["You have one event tomorrow.","You have %{ count } events tomorrow"],"You need to login.":"You need to login.","You're not going to any event yet":"You're not going to any event yet","Your account has been validated":"Your account has been validated","Your account is being validated":"Your account is being validated","Your account is nearly ready, %{username}":"Your account is nearly ready, %{username}","Your local administrator resumed it's policy:":"Your local administrator resumed it's policy:","World map":"World map"},"fr_FR":{"© 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","A validation email was sent to %{email}":"Un email de validation a été envoyé à %{email}","About":"À propos","About this event":"À propos de cet événement","About this instance":"À propos de cette instance","Add a new profile":"Ajouter un nouveau profil","Add to my calendar":"Ajouter à mon agenda","Are you going to this event?":"Allez-vous à cet événement ?","Before you can login, you need to click on the link inside it to validate your account":"Avant que vous puissiez vous enregistrer, vous devez cliquer sur le lien à l'intérieur pour valider votre compte","By %{ name }":"Par %{name}","Create a new event":"Créer un nouvel événement","Create a new group":"Créer un nouveau groupe","Create group":"Créer un groupe","Create my event":"Créer mon événement","Create my group":"Créer mon groupe","Create my profile":"Créer mon profil","Create token":"Créer un jeton","Create your communities and your events":"Créer vos communautés et vos événements","Current":"Actuel","Delete":"Supprimer","Didn't receive the instructions ?":"Vous n'avez pas reçu les instructions ?","Edit":"Éditer","Either the account is already validated, either the validation token is incorrect.":"Soit le compte est déjà validé, soit le jeton de validation est incorrect.","Event list":"Liste d'événements","Events":"Événements","Events nearby you":"Événements près de chez vous","Events you're going at":"Événements auxquels vous vous rendez","Features":"Fonctionnalités","Forgot your password ?":"Mot de passe oublié ?","From the %{ startDate } at %{ startTime } to the %{ endDate } at %{ endTime }":"Du %{ startDate } à %{ startTime } au %{ endDate } à %{ endTime }","Group List":"Liste de groupes","Groups":"Groupes","iCal Feed":"Flux iCal","Identities":"Identités","If an account with this email exists, we just sent another confirmation email to %{email}":"Si un compte avec un tel email existe, nous venons juste d'envoyer un nouvel email de confirmation à %{email}","Join":"Rejoindre","Learn more on\n joinmobilizon.org":"En apprendre plus sur\n joinmobilizon.org","Leave":"Quitter","Legal":"Mentions légales","License":"License","Log in":"Se connecter","Log out":"Se déconnecter","Login":"Se connecter","meditate a bit":"méditez un peu","Members":"Membres","My account":"Mon compte","No address defined":"Aucune adresse définie","No events found":"Aucun événement trouvé","No group found":"Aucun groupe trouvé","No groups found":"Aucun groupe trouvé","Organized":"Organisés","Organizer":"Organisateur","Other stuff…":"Autres trucs…","Password reset":"Réinitialisation du mot de passe","Please be nice to each other":"Soyez sympas entre vous","Please check you spam folder if you didn't receive the email.":"Merci de vérifier votre dossier des indésirables si vous n'avez pas reçu l'email.","Please contact this instance's Mobilizon admin if you think this is a mistake.":"Veuillez contacter l'administrateur de cette instance Mobilizon si vous pensez qu’il s’agit d’une erreur.","Please make sure the address is correct and that the page hasn't been moved.":"Assurez‐vous que l’adresse est correcte et que la page n’a pas été déplacée.","Please read the full rules":"Merci de lire les règles complètes","Private feeds":"Flux privés","public event":"événement public","Public feeds":"Flux publics","Public iCal Feed":"Flux iCal public","Public RSS/Atom Feed":"Flux RSS/Atom public","Register":"S'inscrire","Register an account on Mobilizon!":"S'inscrire sur Mobilizon !","Registration is currently closed.":"Les inscriptions sont actuellement fermées.","Resend confirmation email":"Envoyer à nouveau l'email de confirmation","Reset my password":"Réinitialiser mon mot de passe","RSS/Atom Feed":"Flux RSS/Atom","Search":"Rechercher","Search results: « %{ search } »":"Résultats de recherche : « %{ search } »","Send confirmation email again":"Envoyer l'email de confirmation à nouveau","Send email to reset my password":"Envoyer un email pour réinitialiser mon mot de passe","Share this event":"Partager cet événement","Show map":"Afficher la carte","Sign up":"S'enregistrer","The %{ date } at %{ time }":"Le %{ date } à %{ time }","The %{ date } from %{ startTime } to %{ endTime }":"Le %{ date } de %{ startTime } à %{ endTime }","The event organizer didn't add any description.":"L'organisateur de l'événement n'a pas ajouté de description.","The page you're looking for doesn't exist.":"La page que vous recherchez n'existe pas.","These events may interest you":"Ces événements peuvent vous intéresser","This instance isn't opened to registrations, but you can register on other instances.":"Cette instance n'autorise pas les inscriptions, mais vous pouvez vous enregistrer sur d'autres instances.","Unknown error.":"Erreur inconnue.","User logout":"Déconnexion","We just sent an email to %{email}":"Nous venons d'envoyer un email à %{email}","Welcome back %{username}":"Bon retour %{username}","Welcome back!":"Bon retour !","You announced that you're going to this event.":"Vous avez annoncé vous rendre à cet événement.","You are already logged-in.":"Vous êtes déjà connecté.","You are an organizer.":"Vous êtes un organisateur.","You have one event in %{ days } days.":["Vous avez un événement dans %{ days } jours.","Vous avez %{ count } événements dans %{ days } jours"],"You have one event today.":["Vous avez un événement aujourd'hui.","Vous avez %{ count } événements aujourd'hui"],"You have one event tomorrow.":["Vous avez un événement demain.","Vous avez %{ count } événements demain"],"You need to login.":"Vous devez vous connecter.","You're not going to any event yet":"Vous n'allez à aucun événement pour le moment","Your account has been validated":"Votre compte a été validé","Your account is being validated":"Votre compte est en cours de validation","Your account is nearly ready, %{username}":"Votre compte est presque prêt, %{ username }","Your local administrator resumed it's policy:":"Votre administrateur local a résumé sa politique ainsi :","World map":"Carte mondiale","Search events, groups, etc.":"Rechercher des événements, des groupes, etc."}} \ No newline at end of file +{"en_US":{"© 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","A validation email was sent to %{email}":"A validation email was sent to %{email}","About":"About","About this event":"About this event","About this instance":"About this instance","Add a new profile":"Add a new profile","Add to my calendar":"Add to my calendar","Are you going to this event?":"Are you going to this event?","Before you can login, you need to click on the link inside it to validate your account":"Before you can login, you need to click on the link inside it to validate your account","By %{ name }":"By %{ name }","Create a new event":"Create a new event","Create a new group":"Create a new group","Create group":"Create group","Create my event":"Create my event","Create my group":"Create my group","Create my profile":"Create my profile","Create token":"Create token","Create your communities and your events":"Create your communities and your events","Current":"Current","Delete":"Delete","Didn't receive the instructions ?":"Didn't receive the instructions ?","Edit":"Edit","Either the account is already validated, either the validation token is incorrect.":"Either the account is already validated, either the validation token is incorrect.","Event list":"Event list","Events":"Events","Events nearby you":"Events nearby you","Events you're going at":"Events you're going at","Features":"Features","Forgot your password ?":"Forgot your password ?","From the %{ startDate } at %{ startTime } to the %{ endDate } at %{ endTime }":"From the %{ startDate } at %{ startTime } to the %{ endDate } at %{ endTime }","Group List":"Group List","Groups":"Groups","iCal Feed":"iCal Feed","Identities":"Identities","If an account with this email exists, we just sent another confirmation email to %{email}":"If an account with this email exists, we just sent another confirmation email to %{email}","Join":"Join","Learn more on\n joinmobilizon.org":"Learn more on\n joinmobilizon.org","Leave":"Leave","Legal":"Legal","License":"License","Log in":"Log in","Log out":"Log out","Login":"Login","meditate a bit":"meditate a bit","Members":"Members","My account":"My account","No address defined":"No address defined","No events found":"No events found","No group found":"No group found","No groups found":"No groups found","Organized":"Organized","Organizer":"Organizer","Other stuff…":"Other stuff…","Password reset":"Password reset","Please be nice to each other":"Please be nice to each other","Please check you spam folder if you didn't receive the email.":"Please check you spam folder if you didn't receive the email.","Please read the full rules":"Please read the full rules","Private feeds":"Private feeds","public event":"public event","Public feeds":"Public feeds","Public iCal Feed":"Public iCal Feed","Public RSS/Atom Feed":"Public RSS/Atom Feed","Register":"Register","Register an account on Mobilizon!":"Register an account on Mobilizon!","Registration is currently closed.":"Registration is currently closed.","Resend confirmation email":"Resend confirmation email","Reset my password":"Reset my password","RSS/Atom Feed":"RSS/Atom Feed","Search":"Search","Search results: « %{ search } »":"Search results: « %{ search } »","Send confirmation email again":"Send confirmation email again","Send email to reset my password":"Send email to reset my password","Share this event":"Share this event","Show map":"Show map","Sign up":"Sign up","The %{ date } at %{ time }":"The %{ date } at %{ time }","The %{ date } from %{ startTime } to %{ endTime }":"The %{ date } from %{ startTime } to %{ endTime }","The event organizer didn't add any description.":"The event organizer didn't add any description.","These events may interest you":"These events may interest you","This instance isn't opened to registrations, but you can register on other instances.":"This instance isn't opened to registrations, but you can register on other instances.","Unknown error.":"Unknown error.","User logout":"User logout","We just sent an email to %{email}":"We just sent an email to %{email}","Welcome back %{username}":"Welcome back %{username}","Welcome back!":"Welcome back!","You announced that you're going to this event.":"You announced that you're going to this event.","You are already logged-in.":"You are already logged-in.","You are an organizer.":"You are an organizer.","You have one event in %{ days } days.":["You have one event in %{ days } days.","You have %{ count } events in %{ days } days"],"You have one event today.":["You have one event today.","You have %{ count } events today"],"You have one event tomorrow.":["You have one event tomorrow.","You have %{ count } events tomorrow"],"You need to login.":"You need to login.","You're not going to any event yet":"You're not going to any event yet","Your account has been validated":"Your account has been validated","Your account is being validated":"Your account is being validated","Your account is nearly ready, %{username}":"Your account is nearly ready, %{username}","Your local administrator resumed it's policy:":"Your local administrator resumed it's policy:","World map":"World map"},"fr_FR":{"© 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","A validation email was sent to %{email}":"Un email de validation a été envoyé à %{email}","About":"À propos","About this event":"À propos de cet événement","About this instance":"À propos de cette instance","Add a new profile":"Ajouter un nouveau profil","Add to my calendar":"Ajouter à mon agenda","Are you going to this event?":"Allez-vous à cet événement ?","Before you can login, you need to click on the link inside it to validate your account":"Avant que vous puissiez vous enregistrer, vous devez cliquer sur le lien à l'intérieur pour valider votre compte","By %{ name }":"Par %{name}","Create a new event":"Créer un nouvel événement","Create a new group":"Créer un nouveau groupe","Create group":"Créer un groupe","Create my event":"Créer mon événement","Create my group":"Créer mon groupe","Create my profile":"Créer mon profil","Create token":"Créer un jeton","Create your communities and your events":"Créer vos communautés et vos événements","Current":"Actuel","Delete":"Supprimer","Didn't receive the instructions ?":"Vous n'avez pas reçu les instructions ?","Edit":"Éditer","Either the account is already validated, either the validation token is incorrect.":"Soit le compte est déjà validé, soit le jeton de validation est incorrect.","Event list":"Liste d'événements","Events":"Événements","Events nearby you":"Événements près de chez vous","Events you're going at":"Événements auxquels vous vous rendez","Features":"Fonctionnalités","Forgot your password ?":"Mot de passe oublié ?","From the %{ startDate } at %{ startTime } to the %{ endDate } at %{ endTime }":"Du %{ startDate } à %{ startTime } au %{ endDate } à %{ endTime }","Group List":"Liste de groupes","Groups":"Groupes","iCal Feed":"Flux iCal","Identities":"Identités","If an account with this email exists, we just sent another confirmation email to %{email}":"Si un compte avec un tel email existe, nous venons juste d'envoyer un nouvel email de confirmation à %{email}","Join":"Rejoindre","Learn more on\n joinmobilizon.org":"En apprendre plus sur\n joinmobilizon.org","Leave":"Quitter","Legal":"Mentions légales","License":"License","Log in":"Se connecter","Log out":"Se déconnecter","Login":"Se connecter","meditate a bit":"méditez un peu","Members":"Membres","My account":"Mon compte","No address defined":"Aucune adresse définie","No events found":"Aucun événement trouvé","No group found":"Aucun groupe trouvé","No groups found":"Aucun groupe trouvé","Organized":"Organisés","Organizer":"Organisateur","Other stuff…":"Autres trucs…","Password reset":"Réinitialisation du mot de passe","Please be nice to each other":"Soyez sympas entre vous","Please check you spam folder if you didn't receive the email.":"Merci de vérifier votre dossier des indésirables si vous n'avez pas reçu l'email.","Please contact this instance's Mobilizon admin if you think this is a mistake.":"Veuillez contacter l'administrateur de cette instance Mobilizon si vous pensez qu’il s’agit d’une erreur.","Please make sure the address is correct and that the page hasn't been moved.":"Assurez‐vous que l’adresse est correcte et que la page n’a pas été déplacée.","Please read the full rules":"Merci de lire les règles complètes","Private feeds":"Flux privés","public event":"événement public","Public feeds":"Flux publics","Public iCal Feed":"Flux iCal public","Public RSS/Atom Feed":"Flux RSS/Atom public","Register":"S'inscrire","Register an account on Mobilizon!":"S'inscrire sur Mobilizon !","Registration is currently closed.":"Les inscriptions sont actuellement fermées.","Resend confirmation email":"Envoyer à nouveau l'email de confirmation","Reset my password":"Réinitialiser mon mot de passe","RSS/Atom Feed":"Flux RSS/Atom","Search":"Rechercher","Search results: « %{ search } »":"Résultats de recherche : « %{ search } »","Send confirmation email again":"Envoyer l'email de confirmation à nouveau","Send email to reset my password":"Envoyer un email pour réinitialiser mon mot de passe","Share this event":"Partager l'événement","Show map":"Afficher la carte","Sign up":"S'enregistrer","The %{ date } at %{ time }":"Le %{ date } à %{ time }","The %{ date } from %{ startTime } to %{ endTime }":"Le %{ date } de %{ startTime } à %{ endTime }","The event organizer didn't add any description.":"L'organisateur de l'événement n'a pas ajouté de description.","The page you're looking for doesn't exist.":"La page que vous recherchez n'existe pas.","These events may interest you":"Ces événements peuvent vous intéresser","This instance isn't opened to registrations, but you can register on other instances.":"Cette instance n'autorise pas les inscriptions, mais vous pouvez vous enregistrer sur d'autres instances.","Unknown error.":"Erreur inconnue.","User logout":"Déconnexion","We just sent an email to %{email}":"Nous venons d'envoyer un email à %{email}","Welcome back %{username}":"Bon retour %{username}","Welcome back!":"Bon retour !","You announced that you're going to this event.":"Vous avez annoncé vous rendre à cet événement.","You are already logged-in.":"Vous êtes déjà connecté.","You are an organizer.":"Vous êtes un organisateur.","You have one event in %{ days } days.":["Vous avez un événement dans %{ days } jours.","Vous avez %{ count } événements dans %{ days } jours"],"You have one event today.":["Vous avez un événement aujourd'hui.","Vous avez %{ count } événements aujourd'hui"],"You have one event tomorrow.":["Vous avez un événement demain.","Vous avez %{ count } événements demain"],"You need to login.":"Vous devez vous connecter.","You're not going to any event yet":"Vous n'allez à aucun événement pour le moment","Your account has been validated":"Votre compte a été validé","Your account is being validated":"Votre compte est en cours de validation","Your account is nearly ready, %{username}":"Votre compte est presque prêt, %{ username }","Your local administrator resumed it's policy:":"Votre administrateur local a résumé sa politique ainsi :","World map":"Carte mondiale","Search events, groups, etc.":"Rechercher des événements, des groupes, etc."}} \ No newline at end of file diff --git a/js/src/types/event.model.ts b/js/src/types/event.model.ts index 25289c0b3..a243dd67b 100644 --- a/js/src/types/event.model.ts +++ b/js/src/types/event.model.ts @@ -69,6 +69,8 @@ export interface IEvent { attributedTo: IActor; participants: IParticipant[]; + relatedEvents: IEvent[]; + onlineAddress?: string; phoneAddress?: string; physicalAddress?: IAddress; @@ -94,6 +96,7 @@ export class EventModel implements IEvent { visibility: EventVisibility = EventVisibility.PUBLIC; attributedTo: IActor = new Actor(); organizerActor: IActor = new Actor(); + relatedEvents: IEvent[] = []; onlineAddress: string = ''; phoneAddress: string = ''; } diff --git a/js/src/views/Event/Event.vue b/js/src/views/Event/Event.vue index fb8d77685..0596bf6ad 100644 --- a/js/src/views/Event/Event.vue +++ b/js/src/views/Event/Event.vue @@ -204,12 +204,14 @@

Share this event

- - - - - - +
+ + + + + + +

@@ -223,8 +225,8 @@

These events may interest you

-
- +
+
diff --git a/lib/mobilizon/events/events.ex b/lib/mobilizon/events/events.ex index 8c7c48079..68abb8a2d 100644 --- a/lib/mobilizon/events/events.ex +++ b/lib/mobilizon/events/events.ex @@ -57,6 +57,7 @@ defmodule Mobilizon.Events do e.organizer_actor_id == ^actor_id and e.visibility in [^:public, ^:unlisted] and e.begins_on > ^DateTime.utc_now(), order_by: [asc: :begins_on], + limit: 1, preload: [ :organizer_actor, :tags, @@ -259,18 +260,42 @@ defmodule Mobilizon.Events do [%Event{}, ...] """ - def list_events(page \\ nil, limit \\ nil) do + @spec list_events(integer(), integer(), atom(), atom()) :: list(Event.t()) + def list_events( + page \\ nil, + limit \\ nil, + sort \\ :begins_on, + direction \\ :asc, + unlisted \\ false, + future \\ true + ) do query = from( e in Event, - where: e.visibility == ^:public, preload: [:organizer_actor, :participants] ) |> paginate(page, limit) + |> sort(sort, direction) + |> restrict_future_events(future) + |> allow_unlisted(unlisted) Repo.all(query) end + # Make sure we only show future events + @spec restrict_future_events(Ecto.Query.t(), boolean()) :: Ecto.Query.t() + defp restrict_future_events(query, true), + do: from(q in query, where: q.begins_on > ^DateTime.utc_now()) + + defp restrict_future_events(query, false), do: query + + # Make sure unlisted events don't show up where they're not allowed + @spec allow_unlisted(Ecto.Query.t(), boolean()) :: Ecto.Query.t() + defp allow_unlisted(query, true), + do: from(q in query, where: q.visibility in [^:public, ^:unlisted]) + + defp allow_unlisted(query, false), do: from(q in query, where: q.visibility == ^:public) + @doc """ Find events by name """ diff --git a/lib/mobilizon_web/resolvers/event.ex b/lib/mobilizon_web/resolvers/event.ex index 5ae1710fd..1113a83ca 100644 --- a/lib/mobilizon_web/resolvers/event.ex +++ b/lib/mobilizon_web/resolvers/event.ex @@ -49,7 +49,7 @@ defmodule MobilizonWeb.Resolvers.Event do List related events """ def list_related_events( - %Event{tags: tags, organizer_actor: organizer_actor}, + %Event{tags: tags, organizer_actor: organizer_actor, uuid: uuid}, _args, _resolution ) do @@ -57,12 +57,13 @@ defmodule MobilizonWeb.Resolvers.Event do events = [Events.get_actor_upcoming_public_event(organizer_actor, uuid)] |> Enum.filter(&is_map/1) + # We find similar events with the same tags # 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) + @number_of_related_events )) |> uniq_events() @@ -85,10 +86,11 @@ defmodule MobilizonWeb.Resolvers.Event do # 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 + defp uniq_events(events), do: Enum.uniq_by(events, fn event -> event.uuid end) + @doc """ Join an event for an actor """