Merge branch 'feature/related_events' into 'master'

Feature/related events

See merge request framasoft/mobilizon!113
This commit is contained in:
Thomas Citharel 2019-04-23 11:31:20 +02:00
commit 10bfc17306
13 changed files with 267 additions and 41 deletions

View file

@ -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,
}
}
}
}

View file

@ -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 ""

View file

@ -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."

File diff suppressed because one or more lines are too long

View file

@ -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 = '';
}

View file

@ -204,12 +204,14 @@
<div class="columns">
<div class="column is-half has-text-centered">
<h3 class="title"><translate>Share this event</translate></h3>
<b-icon icon="mastodon" size="is-large" type="is-primary" />
<a :href="facebookShareUrl" target="_blank" rel="nofollow noopener"><b-icon icon="facebook" size="is-large" type="is-primary" /></a>
<a :href="twitterShareUrl" target="_blank" rel="nofollow noopener"><b-icon icon="twitter" size="is-large" type="is-primary" /></a>
<a :href="emailShareUrl" target="_blank" rel="nofollow noopener"><b-icon icon="email" size="is-large" type="is-primary" /></a>
<!-- TODO: mailto: links are not used anymore, we should provide a popup to redact a message instead -->
<a :href="linkedInShareUrl" target="_blank" rel="nofollow noopener"><b-icon icon="linkedin" size="is-large" type="is-primary" /></a>
<div>
<b-icon icon="mastodon" size="is-large" type="is-primary" />
<a :href="facebookShareUrl" target="_blank" rel="nofollow noopener"><b-icon icon="facebook" size="is-large" type="is-primary" /></a>
<a :href="twitterShareUrl" target="_blank" rel="nofollow noopener"><b-icon icon="twitter" size="is-large" type="is-primary" /></a>
<a :href="emailShareUrl" target="_blank" rel="nofollow noopener"><b-icon icon="email" size="is-large" type="is-primary" /></a>
<!-- TODO: mailto: links are not used anymore, we should provide a popup to redact a message instead -->
<a :href="linkedInShareUrl" target="_blank" rel="nofollow noopener"><b-icon icon="linkedin" size="is-large" type="is-primary" /></a>
</div>
</div>
<hr />
<div class="column is-half has-text-right add-to-calendar">
@ -223,8 +225,8 @@
<section class="more-events container">
<h3 class="title has-text-centered"><translate>These events may interest you</translate></h3>
<div class="columns">
<div class="column" v-for="index in 3" :key="index">
<EventCard :event="event" />
<div class="column" v-for="relatedEvent in event.relatedEvents" :key="relatedEvent.uuid">
<EventCard :event="relatedEvent" />
</div>
</div>
</section>

View file

@ -45,6 +45,35 @@ 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],
limit: 1,
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(
@ -231,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
"""
@ -276,6 +329,28 @@ defmodule Mobilizon.Events do
%{total: Task.await(total), elements: Task.await(elements)}
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.

View file

@ -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,52 @@ 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, uuid: uuid},
_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)
# 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
))
|> 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)
{:ok, events}
end
defp uniq_events(events), do: Enum.uniq_by(events, fn event -> event.uuid end)
@doc """
Join an event for an actor
"""

View file

@ -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))

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
}